Skip to content

Commit 9262eec

Browse files
authored
feat(hybrid_wrapper): 增强文本换行功能并修复行内代码处理 (#20)
- 新增段落保留功能,支持按段落处理文本 - 改进行内代码正则表达式,支持单反引号和双反引号 - 修复行内代码重复添加反引号的问题 - 优化列表项换行处理,避免列表标记单独成行 - 添加行内代码换行测试用例 chore: 升级版本号至0.5.2 Signed-off-by: jinlong <jinlong@tencent.com>
1 parent f3c3b28 commit 9262eec

4 files changed

Lines changed: 322 additions & 27 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fastcommit"
3-
version = "0.5.1"
3+
version = "0.5.2"
44
description = "AI-based command line tool to quickly generate standardized commit messages."
55
edition = "2021"
66
authors = ["longjin <fslongjin@vip.qq.com>"]

src/text_wrapper/hybrid_wrapper.rs

Lines changed: 137 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ impl HybridWrapper {
1313
Self {
1414
code_block_regex: Regex::new(r"```[\s\S]*?```").unwrap(),
1515
link_regex: Regex::new(r"https?://[^\s]+|\[([^\]]+)\]\(([^)]+)\)").unwrap(),
16-
inline_code_regex: Regex::new(r"`[^`]+`").unwrap(),
16+
// 支持单反引号和双反引号的行内代码(如 `code` 或 ``code``)
17+
inline_code_regex: Regex::new(r"`{1,3}[^`]+?`{1,3}").unwrap(),
1718
}
1819
}
1920
}
@@ -24,38 +25,120 @@ impl WordWrapper for HybridWrapper {
2425
return String::new();
2526
}
2627

27-
// 解析文本段
28-
let segments = self.parse_segments(text);
28+
// 如果 preserve_paragraphs 为 true,需要先处理段落,然后再处理段
29+
if config.preserve_paragraphs {
30+
let mut result = String::new();
31+
let paragraphs: Vec<&str> = text.split("\n\n").collect();
32+
33+
for (i, paragraph) in paragraphs.iter().enumerate() {
34+
if i > 0 {
35+
result.push_str("\n\n"); // 段落之间保留空行
36+
}
37+
38+
// 检查段落内是否有换行符,如果有则保留
39+
if paragraph.contains('\n') {
40+
let lines: Vec<&str> = paragraph.lines().collect();
41+
for (j, line) in lines.iter().enumerate() {
42+
if j > 0 {
43+
result.push('\n');
44+
}
45+
if !line.trim().is_empty() {
46+
// 对每一行单独处理,但不在段级别使用 preserve_paragraphs
47+
let mut line_config = config.clone();
48+
line_config.preserve_paragraphs = false;
49+
let segments = self.parse_segments(line.trim());
50+
let wrapped_line = self.wrap_segments(&segments, &line_config);
51+
result.push_str(&wrapped_line);
52+
} else {
53+
result.push('\n');
54+
}
55+
}
56+
} else {
57+
// 段落内没有换行符,直接处理整个段落
58+
// 但不在段级别使用 preserve_paragraphs,因为段落处理已经在更高层级完成
59+
let mut para_config = config.clone();
60+
para_config.preserve_paragraphs = false;
61+
let segments = self.parse_segments(paragraph.trim());
62+
let wrapped_paragraph = self.wrap_segments(&segments, &para_config);
63+
result.push_str(&wrapped_paragraph);
64+
}
65+
}
2966

30-
// 处理分段文本
31-
self.wrap_segments(&segments, config)
67+
result
68+
} else {
69+
// 解析文本段
70+
let segments = self.parse_segments(text);
71+
72+
// 处理分段文本
73+
self.wrap_segments(&segments, config)
74+
}
3275
}
3376

3477
fn wrap_segments(&self, segments: &[TextSegment], config: &WrapConfig) -> String {
3578
let mut result = String::new();
3679
let mut current_line = String::new();
3780
let mut current_width = config.indent.width();
3881

39-
for segment in segments {
40-
let processed = self.process_segment(segment, config);
82+
// 检查是否是列表项(第一个段是 PlainText 且以 "- " 开头)
83+
let is_list_item = segments
84+
.first()
85+
.and_then(|s| {
86+
if let TextSegment::PlainText(text) = s {
87+
Some(text.trim_start().starts_with("-"))
88+
} else {
89+
None
90+
}
91+
})
92+
.unwrap_or(false);
93+
94+
for (idx, segment) in segments.iter().enumerate() {
95+
// 对于行内代码,需要特殊处理:作为不可分割的单元
96+
// 行内代码已经包含反引号(从正则匹配中),不需要通过process_segment重复添加
97+
let processed = match segment {
98+
TextSegment::InlineCode(code) => {
99+
// 行内代码已经包含反引号,直接使用,作为不可分割的单元
100+
code.clone()
101+
}
102+
_ => self.process_segment(segment, config),
103+
};
41104

42-
if current_width + processed.width() <= config.max_width {
105+
let processed_width = processed.width();
106+
107+
// 检查当前行是否能放下这个段
108+
// 对于行内代码,如果当前行已经有内容且放不下,需要整体换行
109+
// 对于普通文本,可以继续处理
110+
// 特殊处理:如果是列表项,且当前行只包含列表标记("-"),不要换行
111+
let is_list_marker_only = is_list_item && idx == 0 && current_line.trim() == "-";
112+
let should_wrap =
113+
current_width + processed_width > config.max_width && !is_list_marker_only;
114+
115+
if !should_wrap {
43116
current_line.push_str(&processed);
44-
current_width += processed.width();
117+
current_width += processed_width;
45118
} else {
46119
// 当前行放不下,需要换行
47-
if !current_line.is_empty() {
120+
// 特殊处理:如果是列表项,且当前行只包含列表标记("-"),不要换行
121+
if !current_line.is_empty() && !is_list_marker_only {
48122
result.push_str(&current_line);
49123
result.push('\n');
50124
}
51125

52126
// 新行处理
53-
current_line = config.indent.clone();
54-
if !current_line.is_empty() {
55-
current_line.push_str(&config.hanging_indent);
127+
if is_list_marker_only {
128+
// 当前行只有 "-",不要换行,继续在当前行处理
129+
if !current_line.ends_with(' ') {
130+
current_line.push(' ');
131+
}
132+
current_line.push_str(&processed);
133+
current_width = current_line.width();
134+
} else {
135+
current_line = config.indent.clone();
136+
if !current_line.is_empty() {
137+
current_line.push_str(&config.hanging_indent);
138+
}
139+
current_line.push_str(&processed);
140+
current_width = current_line.width();
56141
}
57-
current_line.push_str(&processed);
58-
current_width = current_line.width();
59142
}
60143
}
61144

@@ -155,7 +238,12 @@ impl HybridWrapper {
155238
match segment {
156239
TextSegment::PlainText(text) => {
157240
if config.handle_code_blocks {
158-
self.wrap_plain_text(text, config)
241+
// 在 wrap_segments 中,PlainText 段不应该使用 wrap_with_paragraphs
242+
// 因为段落处理应该在更高层级(wrap_text)进行
243+
// 这里只处理单词级别的换行,不处理段落
244+
let mut no_paragraph_config = config.clone();
245+
no_paragraph_config.preserve_paragraphs = false;
246+
self.wrap_plain_text(text, &no_paragraph_config)
159247
} else {
160248
text.clone()
161249
}
@@ -175,7 +263,9 @@ impl HybridWrapper {
175263
}
176264
}
177265
TextSegment::InlineCode(code) => {
178-
format!("`{}`", code)
266+
// InlineCode段已经包含反引号(从正则匹配中),直接返回
267+
// 行内代码应该作为不可分割的单元,不进行额外的包装处理
268+
code.clone()
179269
}
180270
}
181271
}
@@ -207,7 +297,8 @@ impl HybridWrapper {
207297
result.push('\n');
208298
}
209299
if !line.trim().is_empty() {
210-
let wrapped_line = self.wrap_without_paragraphs(line.trim(), config);
300+
let trimmed = line.trim();
301+
let wrapped_line = self.wrap_without_paragraphs(trimmed, config);
211302
result.push_str(&wrapped_line);
212303
} else {
213304
result.push('\n');
@@ -237,6 +328,9 @@ impl HybridWrapper {
237328
let word_width = word.width();
238329
let separator_width = if current_line.is_empty() { 0 } else { 1 };
239330

331+
// 特殊处理:如果当前行只包含 "-"(列表项标记),不要换行
332+
let is_list_marker_only = current_line.trim() == "-";
333+
240334
if current_width + separator_width + word_width <= config.max_width {
241335
if !current_line.is_empty() {
242336
current_line.push(' ');
@@ -247,10 +341,17 @@ impl HybridWrapper {
247341
// 当前单词放不下,需要换行
248342
if config.break_long_words && word_width > config.max_width {
249343
// 长单词强制换行
250-
if !current_line.is_empty() {
344+
// 特殊处理:如果当前行只包含 "-",不要换行,而是继续在当前行处理
345+
if !is_list_marker_only && !current_line.is_empty() {
251346
lines.push(current_line);
252347
current_line = String::new();
253348
current_width = config.indent.width();
349+
} else if is_list_marker_only {
350+
// 当前行只有 "-",不要换行,继续在当前行处理长单词
351+
if !current_line.ends_with(' ') {
352+
current_line.push(' ');
353+
current_width += 1;
354+
}
254355
}
255356

256357
let mut remaining = word;
@@ -262,7 +363,7 @@ impl HybridWrapper {
262363
self.break_word_at_width(remaining, available)
263364
};
264365

265-
if !current_line.is_empty() {
366+
if !current_line.is_empty() && !current_line.ends_with(' ') {
266367
current_line.push(' ');
267368
}
268369
current_line.push_str(part);
@@ -279,12 +380,23 @@ impl HybridWrapper {
279380
}
280381
} else {
281382
// 普通换行
282-
if !current_line.is_empty() {
283-
lines.push(current_line);
383+
// 特殊处理:如果当前行只包含 "-",不要换行,而是继续在当前行处理
384+
if is_list_marker_only {
385+
// 当前行只有 "-",不要换行,继续在当前行处理
386+
if !current_line.ends_with(' ') {
387+
current_line.push(' ');
388+
}
389+
current_line.push_str(word);
390+
current_width = current_line.width();
391+
} else {
392+
// 正常换行
393+
if !current_line.is_empty() {
394+
lines.push(current_line);
395+
}
396+
current_line = config.hanging_indent.clone();
397+
current_line.push_str(word);
398+
current_width = current_line.width();
284399
}
285-
current_line = config.hanging_indent.clone();
286-
current_line.push_str(word);
287-
current_width = current_line.width();
288400
}
289401
}
290402
}

0 commit comments

Comments
 (0)