Skip to content

Commit 0b84b05

Browse files
committed
feat(ui): 候选标签换行符渲染为 ↵ 防御单行布局
候选窗口的候选标签是单行控件, candidate.Text 含真实换行符 (\r/\n) 时 会撑破布局或与相邻候选重叠。 - 新增 CandidateNewlineGlyph = '↵', 作为可调常量集中定义 (字体兼容 可调, 与 CmdbarCandidatePrefix 同类设计)。 - 包级 strings.NewReplacer 把 \r\n / \r / \n 替换为单个符号 (\r\n 列在 最前, NewReplacer 按参数顺序优先匹配, 保证 CRLF 折叠为一个符号)。 - 改造 candidateDisplayText 在 candidateNewlineReplacer.Replace 之后再 拼 CmdbarCandidatePrefix; candidate.Text 自身不变, 上屏多行仍多行。 - 防御范围与转义来源无关 — 不论换行符是解码而来还是用户/词库里本就 存在, 渲染层一律替换。
1 parent ba38344 commit 0b84b05

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

wind_input/internal/ui/renderer_layout.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"image"
66
"image/color"
77
"math"
8+
"strings"
89

910
"github.com/huanfeng/wind_input/pkg/config"
1011
)
@@ -16,13 +17,32 @@ import (
1617
// 历史记录和右键菜单文案。
1718
const CmdbarCandidatePrefix = "⚡"
1819

19-
// candidateDisplayText 返回候选实际渲染到候选框的文本。命令直通车候选 (Actions
20-
// 非空) 在 Text 前加 CmdbarCandidatePrefix 作为视觉标识。
20+
// CandidateNewlineGlyph 候选标签中换行符 (\r / \n) 的占位渲染符号。
21+
// 候选框是单行控件, 候选文本含真实换行会撑破布局或与相邻候选重叠,
22+
// 故渲染前统一替换为该符号。candidate.Text 本身不受影响 (上屏仍多行)。
23+
// 不同字体渲染效果可能有差异, 后续可调 (如改用 ⏎ / ¶)。
24+
const CandidateNewlineGlyph = "↵"
25+
26+
// candidateNewlineReplacer 把候选文本里的换行符折叠为 CandidateNewlineGlyph。
27+
// \r\n 列在最前, NewReplacer 按参数顺序优先匹配, 保证 CRLF 折叠为单个符号。
28+
var candidateNewlineReplacer = strings.NewReplacer(
29+
"\r\n", CandidateNewlineGlyph,
30+
"\r", CandidateNewlineGlyph,
31+
"\n", CandidateNewlineGlyph,
32+
)
33+
34+
// candidateDisplayText 返回候选实际渲染到候选框的文本。
35+
// - 换行符 (\r / \n) 替换为 CandidateNewlineGlyph, 保证单行渲染;
36+
// - 命令直通车候选 (Actions 非空) 在文本前加 CmdbarCandidatePrefix。
37+
//
38+
// candidate 自身的 Text 字段保持原状, 仅渲染时变换, 避免污染历史记录与
39+
// 右键菜单文案。
2140
func candidateDisplayText(cand Candidate) string {
41+
text := candidateNewlineReplacer.Replace(cand.Text)
2242
if len(cand.Actions) > 0 {
23-
return CmdbarCandidatePrefix + cand.Text
43+
return CmdbarCandidatePrefix + text
2444
}
25-
return cand.Text
45+
return text
2646
}
2747

2848
// pagerFontSize returns the font size for the pager indicator (e.g. "1/3").
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package ui
2+
3+
import (
4+
"testing"
5+
6+
"github.com/huanfeng/wind_input/internal/cmdbar"
7+
)
8+
9+
func TestCandidateDisplayText_NewlineGlyph(t *testing.T) {
10+
withAction := Candidate{Text: "打开"}
11+
withAction.Actions = make([]cmdbar.ResolvedAction, 1)
12+
13+
cases := []struct {
14+
name string
15+
cand Candidate
16+
want string
17+
}{
18+
{"plain no newline", Candidate{Text: "你好"}, "你好"},
19+
{"lf replaced", Candidate{Text: "行1\n行2"}, "行1" + CandidateNewlineGlyph + "行2"},
20+
{"cr replaced", Candidate{Text: "行1\r行2"}, "行1" + CandidateNewlineGlyph + "行2"},
21+
{"crlf folds to one glyph", Candidate{Text: "行1\r\n行2"}, "行1" + CandidateNewlineGlyph + "行2"},
22+
{"command prefix kept", withAction, CmdbarCandidatePrefix + "打开"},
23+
}
24+
for _, tc := range cases {
25+
t.Run(tc.name, func(t *testing.T) {
26+
if got := candidateDisplayText(tc.cand); got != tc.want {
27+
t.Fatalf("candidateDisplayText = %q, want %q", got, tc.want)
28+
}
29+
})
30+
}
31+
}

0 commit comments

Comments
 (0)