@zhushanwen/pi-ask-user 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-ask-user",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview, inline editor, and optional comments.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -362,6 +362,83 @@ describe("AskUserComponent — Other free-text editor", () => {
|
|
|
362
362
|
});
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
+
// ── 5e2. 多字符粘贴(M2/M7:完整保留,过滤控制字符,emoji 不丢)──
|
|
366
|
+
// 回归 MUST_FIX-M2:handleEditorInput 按 code point 迭代粘贴 chunk,去掉了
|
|
367
|
+
// `c.length === 1` 守卫(它误杀 BMP 之外的代理对,如 emoji)。
|
|
368
|
+
describe("AskUserComponent — multi-char paste in editor", () => {
|
|
369
|
+
it("C-PASTE-1: multi-char chunk pasted at once is fully captured", () => {
|
|
370
|
+
// 终端把整段粘贴文本作为一个 data chunk 投递。修复前只取首字符。
|
|
371
|
+
const { c } = make([singleQ]);
|
|
372
|
+
c.handleInput(DOWN);
|
|
373
|
+
c.handleInput(DOWN);
|
|
374
|
+
c.handleInput(ENTER); // 打开 freeform 编辑器
|
|
375
|
+
c.handleInput("hello world"); // 一次粘贴多字符
|
|
376
|
+
const lines = c.render(60);
|
|
377
|
+
const editorLine = lines.find((l) => l.includes("█"));
|
|
378
|
+
expect(editorLine).toBeDefined();
|
|
379
|
+
// 完整文本保留(非仅首字符 "h")
|
|
380
|
+
expect(editorLine).toContain("hello world");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("C-PASTE-2: paste with emoji (surrogate pair) preserves emoji (M2)", () => {
|
|
384
|
+
// 修复前:`c.length === 1` 守卫把代理对(🐛 length===2)过滤掉,emoji 被静默丢弃。
|
|
385
|
+
const { c } = make([singleQ]);
|
|
386
|
+
c.handleInput(DOWN);
|
|
387
|
+
c.handleInput(DOWN);
|
|
388
|
+
c.handleInput(ENTER);
|
|
389
|
+
c.handleInput("fix the 🐛 bug");
|
|
390
|
+
const lines = c.render(60);
|
|
391
|
+
const editorLine = lines.find((l) => l.includes("█"));
|
|
392
|
+
expect(editorLine).toBeDefined();
|
|
393
|
+
// emoji 完整保留,不丢失
|
|
394
|
+
expect(editorLine).toContain("fix the 🐛 bug");
|
|
395
|
+
expect(editorLine).toContain("🐛");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("C-PASTE-3: multi-char chunk with control chars filters control, keeps printable", () => {
|
|
399
|
+
// 粘贴 "ab\tcd":\t(U+0009)< 空格 → 被过滤;a/b/c/d 保留。
|
|
400
|
+
const { c } = make([singleQ]);
|
|
401
|
+
c.handleInput(DOWN);
|
|
402
|
+
c.handleInput(DOWN);
|
|
403
|
+
c.handleInput(ENTER);
|
|
404
|
+
c.handleInput("ab\tcd");
|
|
405
|
+
const lines = c.render(60);
|
|
406
|
+
const editorLine = lines.find((l) => l.includes("█"));
|
|
407
|
+
expect(editorLine).toBeDefined();
|
|
408
|
+
// 可打印字符保留,控制字符 \t 被过滤
|
|
409
|
+
expect(editorLine).toContain("abcd");
|
|
410
|
+
expect(editorLine).not.toContain("\t");
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("C-PASTE-4: empty string input is a no-op (no side effects)", () => {
|
|
414
|
+
const { c, result } = make([singleQ]);
|
|
415
|
+
c.handleInput(DOWN);
|
|
416
|
+
c.handleInput(DOWN);
|
|
417
|
+
c.handleInput(ENTER); // 打开编辑器
|
|
418
|
+
const before = c.render(60);
|
|
419
|
+
c.handleInput(""); // 空字符串,无副作用
|
|
420
|
+
const after = c.render(60);
|
|
421
|
+
// 未解析、未崩溃、视图不变
|
|
422
|
+
expect(result.val).toBeUndefined();
|
|
423
|
+
expect(after).toEqual(before);
|
|
424
|
+
// 编辑器仍在(光标在),editorText 仍为空
|
|
425
|
+
expect(after.some((l) => l.includes("█"))).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("C-PASTE-5: single printable char still works (backward-compat)", () => {
|
|
429
|
+
// 回归:修复去掉守卫后单字符行为不变(与 C-27 等价的逐字符输入)。
|
|
430
|
+
const { c } = make([singleQ]);
|
|
431
|
+
c.handleInput(DOWN);
|
|
432
|
+
c.handleInput(DOWN);
|
|
433
|
+
c.handleInput(ENTER);
|
|
434
|
+
c.handleInput("x"); // 单字符
|
|
435
|
+
const lines = c.render(60);
|
|
436
|
+
const editorLine = lines.find((l) => l.includes("█"));
|
|
437
|
+
expect(editorLine).toBeDefined();
|
|
438
|
+
expect(editorLine).toContain("x");
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
365
442
|
// ── 5f. 评论流程(FR-4.6 / FR-11 / AC-6/12/17)─────────
|
|
366
443
|
describe("AskUserComponent — comment flow", () => {
|
|
367
444
|
it("C-33: single-select + allowComment Enter enters comment mode", () => {
|
|
@@ -271,11 +271,12 @@ describe("renderQuestionView — Other editor mode", () => {
|
|
|
271
271
|
true,
|
|
272
272
|
huge,
|
|
273
273
|
);
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
274
|
+
// 统计含输入内容的行数 = input 渲染行数(应被截到 5 行)
|
|
275
|
+
// 编号 "3. " 现在是 styled 内容的一部分,首行包裹段 "3." 不含 'y'
|
|
276
|
+
const inputLines = lines.filter((l) => l.includes("y") || l.includes("3."));
|
|
277
|
+
expect(inputLines.length).toBe(5);
|
|
277
278
|
// 最后一行带省略号(表示还有更多,光标已被省略号取代)
|
|
278
|
-
expect(
|
|
279
|
+
expect(inputLines[inputLines.length - 1]).toContain("…");
|
|
279
280
|
});
|
|
280
281
|
|
|
281
282
|
it("Q-30: 已保存 freeText 预览超屏宽时多行换行展示", () => {
|
package/src/component.ts
CHANGED
|
@@ -382,9 +382,21 @@ export class AskUserComponent implements Component {
|
|
|
382
382
|
this.tui.requestRender();
|
|
383
383
|
return;
|
|
384
384
|
}
|
|
385
|
-
// Printable char
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
// Printable char(s) — handle both single keystrokes and multi-char paste.
|
|
386
|
+
// Terminals deliver pasted text as a single data chunk; iterating each char
|
|
387
|
+
// ensures the full paste is captured instead of silently dropping everything
|
|
388
|
+
// after the first character.
|
|
389
|
+
// for...of 按 code point 迭代:代理对(如 😀 U+1F600)作为一个 c(length===2)出现,
|
|
390
|
+
// 因此不能用 `c.length === 1` 守卫——那会把所有 BMP 之外的字符(emoji、部分 CJK 扩展)全过滤。
|
|
391
|
+
// 只保留 `c >= " "`(code point 比较),过滤控制字符(< 空格 U+0020)。
|
|
392
|
+
let changed = false;
|
|
393
|
+
for (const c of data) {
|
|
394
|
+
if (c >= " ") {
|
|
395
|
+
this.editorText += c;
|
|
396
|
+
changed = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (changed) {
|
|
388
400
|
this.invalidate();
|
|
389
401
|
this.tui.requestRender();
|
|
390
402
|
return;
|
package/src/question-view.ts
CHANGED
|
@@ -107,14 +107,14 @@ function buildOptionLines(
|
|
|
107
107
|
|
|
108
108
|
if (isOther) {
|
|
109
109
|
if (state.mode === "freeform") {
|
|
110
|
-
// 原地编辑:与普通选项的 [ ]
|
|
111
|
-
// lead = "> [ ]
|
|
110
|
+
// 原地编辑:与普通选项的 [ ] 框视觉对齐(单选无勾选语义则留空)。
|
|
111
|
+
// lead = "> [ ] ",编号 + 输入文本跟在框后,内容起始列与普通选项一致。
|
|
112
112
|
const box = q.multiSelect ? t.fg("dim", "[ ]") : " ";
|
|
113
113
|
const num = i + 1;
|
|
114
|
-
const lead = `${prefix} ${box}
|
|
114
|
+
const lead = `${prefix} ${box} `;
|
|
115
115
|
const avail = Math.max(1, width - visibleWidth(lead));
|
|
116
|
-
// 文本 + 末尾光标 █ 整体软换行(空 input
|
|
117
|
-
const styled = `${t.fg("text", editorText)}${t.fg("accent", "█")}`;
|
|
116
|
+
// 编号 + 文本 + 末尾光标 █ 整体软换行(空 input 时仅编号 + 光标,wrapTextWithAnsi 单行)
|
|
117
|
+
const styled = `${t.fg("muted", `${num}. `)}${t.fg("text", editorText)}${t.fg("accent", "█")}`;
|
|
118
118
|
addWrappedInput(add, lead, styled, avail, MAX_EDITOR_LINES);
|
|
119
119
|
} else {
|
|
120
120
|
const hasFreeText = state.freeTextValue !== null;
|
|
@@ -124,7 +124,7 @@ function buildOptionLines(
|
|
|
124
124
|
add(`${prefix} ${check} ${t.fg(labelColor, `${num}. ${opt.label}`)}`);
|
|
125
125
|
if (hasFreeText) {
|
|
126
126
|
// 已保存 freeText 预览:软换行展示,最多 MAX_EDITOR_LINES 行(不再单行截断)
|
|
127
|
-
const lead = "
|
|
127
|
+
const lead = " "; // 6 列缩进,与上方 label 行对齐(prefix1 + sp1 + check1 + sp1 + num2 = 6)
|
|
128
128
|
const avail = Math.max(1, width - visibleWidth(lead));
|
|
129
129
|
const styled = t.fg("dim", `"${state.freeTextValue ?? ""}"`);
|
|
130
130
|
addWrappedInput(add, lead, styled, avail, MAX_EDITOR_LINES);
|
|
@@ -137,8 +137,8 @@ function buildOptionLines(
|
|
|
137
137
|
const num = i + 1;
|
|
138
138
|
add(`${prefix} ${box} ${t.fg(labelColor, `${num}. ${opt.label}`)}`);
|
|
139
139
|
if (opt.description && !hideDescriptions) {
|
|
140
|
-
const wrapped = wrapTextWithAnsi(t.fg("muted", opt.description), width -
|
|
141
|
-
for (const line of wrapped) add(`
|
|
140
|
+
const wrapped = wrapTextWithAnsi(t.fg("muted", opt.description), width - 10);
|
|
141
|
+
for (const line of wrapped) add(` ${line}`);
|
|
142
142
|
}
|
|
143
143
|
} else {
|
|
144
144
|
const isConfirmed = state.selectedIndex === i;
|
|
@@ -147,8 +147,8 @@ function buildOptionLines(
|
|
|
147
147
|
const num = i + 1;
|
|
148
148
|
add(`${prefix} ${check} ${t.fg(labelColor, `${num}. ${opt.label}`)}`);
|
|
149
149
|
if (opt.description && !hideDescriptions) {
|
|
150
|
-
const wrapped = wrapTextWithAnsi(t.fg("muted", opt.description), width -
|
|
151
|
-
for (const line of wrapped) add(`
|
|
150
|
+
const wrapped = wrapTextWithAnsi(t.fg("muted", opt.description), width - 8);
|
|
151
|
+
for (const line of wrapped) add(` ${line}`);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
}
|