Skip to content

Commit ffb2ea5

Browse files
authored
[0027] 修复 gf fmt 对纯分号注释和开头空行的格式化问题 (#789)
1 parent a13f79c commit ffb2ea5

5 files changed

Lines changed: 221 additions & 67 deletions

File tree

devel/0027.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# [0027] 修复 gf fmt 对纯分号注释分隔线的格式化问题
2+
3+
## 任务相关的代码文件
4+
- `tools/fmt/liii/goldfmt-format.scm`
5+
- `tools/fmt/tests/liii/goldfmt-format/format-string-test.scm`
6+
7+
## 如何测试
8+
```bash
9+
# 构建
10+
xmake b goldfish
11+
12+
# 运行相关测试
13+
bin/gf test tools/fmt/tests/liii/goldfmt-format/format-string-test.scm
14+
15+
# 验证纯分号注释保持原样
16+
bin/gf fmt --dry-run tests/fmt_comment_test.scm
17+
```
18+
19+
## 2026-05-11 修复纯分号注释分隔线的格式化
20+
21+
### What
22+
修复 `gf fmt` 对以分号开头的注释内容(包括纯分号分隔线和 `;;;` 开头的注释)的格式化问题。
23+
24+
原行为:
25+
```
26+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; → ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
27+
;;; certain Scheme versions do not define 'filter' → ;; ; certain Scheme versions do not define 'filter'
28+
```
29+
30+
格式化后在 `;;` 和后续分号之间插入了空格,破坏了视觉分隔线的效果,也把 `;;;` 注释拆散了。
31+
32+
修复后行为:
33+
```
34+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; → ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
35+
;;; certain Scheme versions do not define 'filter' → ;;; certain Scheme versions do not define 'filter'
36+
```
37+
38+
以分号开头的注释内容保持连续输出,不在中间插入空格。
39+
40+
### Why
41+
纯分号行通常用作代码中的视觉分隔线或装饰线。`;;;` 开头的注释也是常见的 Scheme 注释风格(通常用于文件级或节级注释)。在 `;;` 和内容之间加空格会破坏这些视觉效果,因此需要特殊处理。
42+
43+
### How
44+
修改 `tools/fmt/liii/goldfmt-format.scm` 中的 `format-comment-content` 函数:
45+
46+
当注释内容以 `;` 字符开头时,不再在 `;;` 和内容之间插入空格,而是直接拼接为 `;;` + content。
47+
48+
```scheme
49+
(define (format-comment-content content)
50+
(if (or (string=? content "")
51+
(char=? (string-ref content 0) #\space)
52+
(char=? (string-ref content 0) #\;)
53+
)
54+
(string-append ";;" content)
55+
(string-append ";; " content)
56+
)
57+
)
58+
```
59+
60+
### 2026-05-11 修复文件开头空行被移除的问题
61+
62+
#### What
63+
修复 `gf fmt` 格式化时,文件开头的空行(如换行后紧跟注释)会被意外移除的问题。
64+
65+
原行为:
66+
```
67+
\n;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; → ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
68+
```
69+
70+
格式化后丢失了文件开头的空行。
71+
72+
修复后行为:
73+
```
74+
\n;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; → \n;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
75+
```
76+
77+
文件开头的空行被正确保留。
78+
79+
#### Why
80+
某些文件在协议头或注释之前会有意保留空行,以形成视觉分隔。格式化工具不应擅自移除这些空行。
81+
82+
#### How
83+
修改 `tools/fmt/liii/goldfmt-format.scm` 中的 `join-top-level` 函数:
84+
85+
原实现会跳过开头的空字符串 piece,导致空行丢失。修改为使用 `first` 标志跟踪是否处于第一个 piece,对第一个 piece 直接拼接而不前置换行符,从而正确保留开头的空行。
86+
87+
```scheme
88+
(define (join-top-level pieces)
89+
(let loop
90+
((rest pieces) (result "") (first #t))
91+
(cond ((null? rest)
92+
(if (and (not (string=? result "")) (not (string-suffix? "\n" result)))
93+
(string-append result "\n")
94+
result
95+
)
96+
)
97+
(first
98+
(loop (cdr rest) (string-append result (car rest)) #f)
99+
)
100+
(else (loop (cdr rest) (string-append result "\n" (car rest)) #f))
101+
)
102+
)
103+
)
104+
```
105+
106+
同时移除了 `format-top-level-nodes` 中针对首个 newline 节点的特殊处理(`make-newlines (+ n 1)`),因为现在 `join-top-level` 能正确处理开头的空 piece。
107+
108+
补充了 5 个单元测试:
109+
1. 纯分号分隔线注释保持原样
110+
2. 内容以分号开头的注释保持连续分号(对应原始 `;;;` 输入)
111+
3. 空注释保持原样
112+
4. 三个分号开头的注释保持原样
113+
5. 文件开头空行后紧跟注释,空行应被保留

tools/fmt/liii/goldfmt-format.scm

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
format-datum+node
2020
format-node
2121
format-string
22+
format-nodes
2223
format-inline
2324
can-inline?
2425
) ;export
@@ -146,7 +147,10 @@
146147
) ;define
147148

148149
(define (format-comment-content content)
149-
(if (or (string=? content "") (char=? (string-ref content 0) #\space))
150+
(if (or (string=? content "")
151+
(char=? (string-ref content 0) #\space)
152+
(char=? (string-ref content 0) #\;)
153+
)
150154
(string-append ";;" content)
151155
(string-append ";; " content)
152156
) ;if
@@ -1058,15 +1062,17 @@
10581062

10591063
(define (join-top-level pieces)
10601064
(let loop
1061-
((rest pieces) (result ""))
1065+
((rest pieces) (result "") (first #t))
10621066
(cond ((null? rest)
10631067
(if (and (not (string=? result "")) (not (string-suffix? "\n" result)))
10641068
(string-append result "\n")
10651069
result
10661070
) ;if
10671071
) ;
1068-
((string=? result "") (loop (cdr rest) (car rest)))
1069-
(else (loop (cdr rest) (string-append result "\n" (car rest))))
1072+
(first
1073+
(loop (cdr rest) (string-append result (car rest)) #f)
1074+
) ;
1075+
(else (loop (cdr rest) (string-append result "\n" (car rest)) #f))
10701076
) ;cond
10711077
) ;let
10721078
) ;define
@@ -1138,18 +1144,22 @@
11381144
(reverse result)
11391145
(let ((node (vector-ref nodes i)))
11401146
(if (newline-node? node)
1141-
(if is-first
1142-
(loop (+ i 1) #t result)
1143-
(loop (+ i 1) #f (cons (make-newlines (newline-count node)) result))
1144-
) ;if
1147+
(loop (+ i 1) #f (cons (make-newlines (newline-count node)) result))
1148+
;if
11451149
(call-with-values (lambda () (format-node node 0))
11461150
(lambda (formatted positioned-node)
1147-
(let ((next-result (if (and (define-node? node) (not is-first))
1148-
(cons formatted (cons "" result))
1149-
(cons formatted result)
1150-
) ;if
1151-
) ;next-result
1152-
) ;
1151+
(let* ((prev-node (if (> i 0) (vector-ref nodes (- i 1)) #f))
1152+
(needs-blank (and (define-node? node)
1153+
(not is-first)
1154+
(not (and prev-node (newline-node? prev-node)))
1155+
)
1156+
) ;needs-blank
1157+
(next-result (if needs-blank
1158+
(cons formatted (cons "" result))
1159+
(cons formatted result)
1160+
) ;if
1161+
) ;next-result
1162+
) ;
11531163
(loop (+ i 1) #f next-result)
11541164
) ;let
11551165
) ;lambda
@@ -1165,4 +1175,8 @@
11651175
(join-top-level pieces)
11661176
) ;let*
11671177
) ;define
1178+
1179+
(define (format-nodes nodes)
1180+
(join-top-level (format-top-level-nodes nodes))
1181+
) ;define
11681182
) ;define-library

tools/fmt/liii/goldfmt-scan.scm

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -697,15 +697,32 @@
697697
) ;define
698698
(define (scan-file path)
699699
(let* ((raw-content (path-read-text path))
700-
(tokens (let ((scanned (source-tokenize raw-content)))
701-
(if (and (not (null? scanned))
700+
(scanned (source-tokenize raw-content))
701+
;; 处理文件开头空行
702+
(leading-blanks (let loop ((i 0) (count 0))
703+
(if (>= i (string-length raw-content))
704+
count
705+
(let ((c (string-ref raw-content i)))
706+
(cond ((char=? c #\newline) (loop (+ i 1) (+ count 1)))
707+
((char=? c #\return) (loop (+ i 1) count))
708+
((or (char=? c #\space) (char=? c #\tab)) (loop (+ i 1) count))
709+
(else count)
710+
) ;cond
711+
) ;let
712+
) ;if
713+
) ;let
714+
)
715+
(tokens-with-leading (if (> leading-blanks 0)
716+
(cons (cons 'newline leading-blanks) scanned)
717+
scanned))
718+
;; 处理文件末尾换行符
719+
(tokens (if (and (not (null? tokens-with-leading))
702720
(> (string-length raw-content) 0)
703-
(char=? (string-ref raw-content (- (string-length raw-content) 1)) #\newline)
704-
) ;and
705-
(append scanned (list (cons 'newline 1)))
706-
scanned
707-
) ;if
708-
) ;let
721+
(char=? (string-ref raw-content (- (string-length raw-content) 1)) #\newline))
722+
;and
723+
(append tokens-with-leading (list (cons 'newline 1)))
724+
tokens-with-leading)
725+
;if
709726
) ;tokens
710727
(processed-content (source-tokens->string tokens))
711728
) ;

tools/fmt/liii/goldfmt.scm

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -94,61 +94,28 @@
9494

9595
;; ; 格式化单个文件(dry-run 模式,输出到终端)
9696
(define (format-file-dry-run path-str)
97-
(let* ((nodes (scan-file path-str)))
98-
(let loop
99-
((i 0))
100-
(if (>= i (vector-length nodes))
101-
(values)
102-
(let ((node (vector-ref nodes i)))
103-
(call-with-values (lambda () (format-node node 0))
104-
(lambda (text positioned-node)
105-
(display (ensure-trailing-newline text))
106-
(loop (+ i 1))
107-
) ;lambda
108-
) ;call-with-values
109-
) ;let
110-
) ;if
111-
) ;let
97+
(let* ((nodes (scan-file path-str))
98+
(formatted (format-nodes nodes))
99+
) ;
100+
(display formatted)
112101
) ;let*
113102
) ;define
114103

115-
;; ; 辅助函数:确保字符串以换行符结尾
116-
(define (ensure-trailing-newline str)
117-
(if (or (string=? str "") (string-suffix? "\n" str))
118-
str
119-
(string-append str "\n")
120-
) ;if
121-
) ;define
122-
123104
;; ; 格式化单个文件(覆盖原文件)
124105
;; ; 返回值: 如果文件有变更返回 #t,否则返回 #f
125106
(define (format-file path-str)
126107
(let* ((p (path path-str))
127108
(original-content (path-read-text p))
128109
(nodes (scan-file path-str))
129-
(results '())
110+
(formatted (format-nodes nodes))
130111
) ;
131-
(let loop
132-
((i 0) (acc '()))
133-
(if (>= i (vector-length nodes))
134-
(let* ((joined (string-join (reverse acc) "\n"))
135-
(formatted (ensure-trailing-newline joined))
136-
) ;
137-
(if (string=? original-content formatted)
138-
#f
139-
(begin
140-
(path-write-text p formatted)
141-
#t
142-
) ;begin
143-
) ;if
144-
) ;let*
145-
(let ((node (vector-ref nodes i)))
146-
(call-with-values (lambda () (format-node node 0))
147-
(lambda (text positioned-node) (loop (+ i 1) (cons text acc)))
148-
) ;call-with-values
149-
) ;let
150-
) ;if
151-
) ;let
112+
(if (string=? original-content formatted)
113+
#f
114+
(begin
115+
(path-write-text p formatted)
116+
#t
117+
) ;begin
118+
) ;if
152119
) ;let*
153120
) ;define
154121

tools/fmt/tests/liii/goldfmt-format/format-string-test.scm

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(import (liii check)
22
(liii goldfmt-format)
33
(liii goldfmt-scan)
4+
(liii goldfmt-record)
45
(liii os)
56
(liii raw-string)
67
(srfi srfi-13)
@@ -217,4 +218,46 @@
217218
"(check (f '((\"messages\"\n . #(((\"role\" . \"user\")\n (\"content\"\n . #(((\"text\" . \"1\") (\"type\" . \"text\"))\n ((\"text\" . \"2\") (\"type\" . \"text\"))\n ) ;#\n ))\n ((\"role\" . \"user\") (\"content\" . \"中文\"))\n ) ;#\n ))\n ) ;f\n =>\n \"ok\"\n) ;check\n"
218219
) ;check
219220

221+
;; 纯分号分隔线注释不应在中间插入空格
222+
(let ((semicolon-line (make-env :tag-name "*comment*" :children (vector (make-atom :value ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")))))
223+
(check (call-with-values (lambda () (format-node semicolon-line 0))
224+
(lambda (text positioned-node) text))
225+
=>
226+
";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"
227+
) ;check
228+
) ;let
229+
230+
;; 内容以分号开头的注释(对应原始输入 ;;; ...)应保持连续分号
231+
(let ((mixed-comment (make-env :tag-name "*comment*" :children (vector (make-atom :value "; not all semicolons")))))
232+
(check (call-with-values (lambda () (format-node mixed-comment 0))
233+
(lambda (text positioned-node) text))
234+
=>
235+
";;; not all semicolons"
236+
) ;check
237+
) ;let
238+
239+
;; 空注释
240+
(let ((empty-comment (make-env :tag-name "*comment*" :children (vector (make-atom :value "")))))
241+
(check (call-with-values (lambda () (format-node empty-comment 0))
242+
(lambda (text positioned-node) text))
243+
=>
244+
";;"
245+
) ;check
246+
) ;let
247+
248+
;; 三个分号开头的注释应保持原样
249+
(let ((triple-comment (make-env :tag-name "*comment*" :children (vector (make-atom :value "; certain Scheme versions do not define 'filter'")))))
250+
(check (call-with-values (lambda () (format-node triple-comment 0))
251+
(lambda (text positioned-node) text))
252+
=>
253+
";;; certain Scheme versions do not define 'filter'"
254+
) ;check
255+
) ;let
256+
257+
;; 测试文件开头空行后紧跟注释,空行应被保留
258+
(check (format-string "(*newline* 1)\n(*comment* \";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\")\n")
259+
=>
260+
"\n;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n"
261+
) ;check
262+
220263
(check-report)

0 commit comments

Comments
 (0)