@zhushanwen/pi-ask-user 0.0.3 → 0.0.4
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.4",
|
|
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",
|
|
@@ -437,6 +437,39 @@ describe("AskUserComponent — multi-char paste in editor", () => {
|
|
|
437
437
|
expect(editorLine).toBeDefined();
|
|
438
438
|
expect(editorLine).toContain("x");
|
|
439
439
|
});
|
|
440
|
+
|
|
441
|
+
it("C-PASTE-6: bracketed paste 序列被剥离,不残留 [200~/[201~", () => {
|
|
442
|
+
// 回归:启用 bracketed paste mode 的终端粘贴时会把内容包裹在
|
|
443
|
+
// \x1b[200~ ... \x1b[201~ 中。ESC 被守卫过滤,但 [200~/[201~ 可见字符
|
|
444
|
+
// 会残留。修复:handleEditorInput 先 replace 剥离这两个序列。
|
|
445
|
+
const { c } = make([singleQ]);
|
|
446
|
+
c.handleInput(DOWN);
|
|
447
|
+
c.handleInput(DOWN);
|
|
448
|
+
c.handleInput(ENTER); // 打开 freeform
|
|
449
|
+
c.handleInput("\x1b[200~hello\x1b[201~");
|
|
450
|
+
const lines = c.render(60);
|
|
451
|
+
const editorLine = lines.find((l) => l.includes("█"));
|
|
452
|
+
expect(editorLine).toBeDefined();
|
|
453
|
+
expect(editorLine).toContain("hello");
|
|
454
|
+
expect(editorLine).not.toContain("[200~");
|
|
455
|
+
expect(editorLine).not.toContain("[201~");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("C-PASTE-7: bracketed paste 跨 chunk 抵达时每个 chunk 独立剥离", () => {
|
|
459
|
+
// 边界:粘贴内容分多个 data chunk 抵达,每个 chunk 各自带始/末标记。
|
|
460
|
+
// 简单 replace 在此场景仍有效(每个 chunk 独立剥离自己的标记)。
|
|
461
|
+
const { c } = make([singleQ]);
|
|
462
|
+
c.handleInput(DOWN);
|
|
463
|
+
c.handleInput(DOWN);
|
|
464
|
+
c.handleInput(ENTER);
|
|
465
|
+
c.handleInput("\x1b[200~foo ");
|
|
466
|
+
c.handleInput(" bar\x1b[201~");
|
|
467
|
+
const lines = c.render(60);
|
|
468
|
+
const editorLine = lines.find((l) => l.includes("█"));
|
|
469
|
+
expect(editorLine).toContain("foo bar");
|
|
470
|
+
expect(editorLine).not.toContain("[200~");
|
|
471
|
+
expect(editorLine).not.toContain("[201~");
|
|
472
|
+
});
|
|
440
473
|
});
|
|
441
474
|
|
|
442
475
|
// ── 5f. 评论流程(FR-4.6 / FR-11 / AC-6/12/17)─────────
|
|
@@ -374,6 +374,99 @@ describe("renderQuestionView — help line", () => {
|
|
|
374
374
|
});
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
+
// ── Q-32 ~ Q-35: Other 选项对齐(单选/多选 freeform、非 freeform、预览) ──
|
|
378
|
+
describe("renderQuestionView — Other row alignment", () => {
|
|
379
|
+
it("Q-32: 单选 Other freeform 编辑行编号与普通选项对齐", () => {
|
|
380
|
+
// 回归:单选 freeform 占位从 \" \"(2列) 改为 \" \"(1列),编号列与普通单选对齐
|
|
381
|
+
const lines = renderQuestionView(
|
|
382
|
+
singleQ,
|
|
383
|
+
makeState({ mode: "freeform", cursorIndex: 2 }),
|
|
384
|
+
stubTheme,
|
|
385
|
+
60,
|
|
386
|
+
true,
|
|
387
|
+
"custom",
|
|
388
|
+
);
|
|
389
|
+
const normalLine = lines.find((l) => l.includes("Postgres"))!;
|
|
390
|
+
const otherLine = lines.find((l) => l.includes("█"))!;
|
|
391
|
+
// stubTheme 无 ANSI,indexOf 反映可见列位置。普通选项编号 idx === Other 编号 idx
|
|
392
|
+
expect(normalLine.indexOf("1.")).toBe(otherLine.indexOf("3."));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("Q-33: 多选 Other(无 freeText)编号与普通选项对齐", () => {
|
|
396
|
+
// 回归:多选 Other 非 freeform 标记从 check(1列) 改为 box(3列),编号列与普通多选对齐
|
|
397
|
+
const multiQ: Question = {
|
|
398
|
+
question: "Which features?",
|
|
399
|
+
multiSelect: true,
|
|
400
|
+
options: [{ label: "Auth" }, { label: "Search" }],
|
|
401
|
+
};
|
|
402
|
+
const lines = renderQuestionView(
|
|
403
|
+
multiQ,
|
|
404
|
+
makeState({ cursorIndex: 2 }),
|
|
405
|
+
stubTheme,
|
|
406
|
+
60,
|
|
407
|
+
true,
|
|
408
|
+
"",
|
|
409
|
+
);
|
|
410
|
+
const normalLine = lines.find((l) => l.includes("Auth"))!;
|
|
411
|
+
const otherLine = lines.find((l) => l.includes("Other") && l.includes("3."))!;
|
|
412
|
+
expect(normalLine.indexOf("1.")).toBe(otherLine.indexOf("3."));
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("Q-34: 单选 Other freeText 预览缩进对齐到 label 起始列", () => {
|
|
416
|
+
// 回归:预览 lead 从硬编码 6 列改为动态计算(单选个位 = 7 列)
|
|
417
|
+
const lines = renderQuestionView(
|
|
418
|
+
singleQ,
|
|
419
|
+
makeState({ cursorIndex: 2, freeTextValue: "saved" }),
|
|
420
|
+
stubTheme,
|
|
421
|
+
60,
|
|
422
|
+
true,
|
|
423
|
+
"",
|
|
424
|
+
);
|
|
425
|
+
const labelLine = lines.find((l) => l.includes("Other") && !l.includes('"'))!;
|
|
426
|
+
const previewLine = lines.find((l) => l.includes('"saved"'))!;
|
|
427
|
+
expect(labelLine.indexOf("Other")).toBe(previewLine.indexOf('"'));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("Q-35: 多选 Other freeText 预览缩进对齐到 label 起始列", () => {
|
|
431
|
+
// 回归:多选 marker=3列,预览 lead 应 = 9 列(个位编号),与 label 对齐
|
|
432
|
+
const multiQ: Question = {
|
|
433
|
+
question: "Which features?",
|
|
434
|
+
multiSelect: true,
|
|
435
|
+
options: [{ label: "Auth" }, { label: "Search" }],
|
|
436
|
+
};
|
|
437
|
+
const lines = renderQuestionView(
|
|
438
|
+
multiQ,
|
|
439
|
+
makeState({ cursorIndex: 2, freeTextValue: "saved" }),
|
|
440
|
+
stubTheme,
|
|
441
|
+
60,
|
|
442
|
+
true,
|
|
443
|
+
"",
|
|
444
|
+
);
|
|
445
|
+
const labelLine = lines.find((l) => l.includes("Other") && !l.includes('"'))!;
|
|
446
|
+
const previewLine = lines.find((l) => l.includes('"saved"'))!;
|
|
447
|
+
expect(labelLine.indexOf("Other")).toBe(previewLine.indexOf('"'));
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("Q-35b: 两位数编号时预览缩进随编号宽度自适应", () => {
|
|
451
|
+
// 10 个选项 + Other = 第 11 项,编号 \"11.\" 占 3 列,预览 lead 应比个位多 1 列
|
|
452
|
+
const bigQ: Question = {
|
|
453
|
+
question: "Q",
|
|
454
|
+
options: Array.from({ length: 10 }, (_, k) => ({ label: `Opt${k + 1}` })),
|
|
455
|
+
};
|
|
456
|
+
const lines = renderQuestionView(
|
|
457
|
+
bigQ,
|
|
458
|
+
makeState({ cursorIndex: 10, freeTextValue: "x" }),
|
|
459
|
+
stubTheme,
|
|
460
|
+
60,
|
|
461
|
+
true,
|
|
462
|
+
"",
|
|
463
|
+
);
|
|
464
|
+
const labelLine = lines.find((l) => l.includes("Other") && !l.includes('"'))!;
|
|
465
|
+
const previewLine = lines.find((l) => l.includes('"x"'))!;
|
|
466
|
+
expect(labelLine.indexOf("Other")).toBe(previewLine.indexOf('"'));
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
377
470
|
// ── Q-24 ~ Q-25: 上下文 ─────────────────────────────────
|
|
378
471
|
describe("renderQuestionView — context", () => {
|
|
379
472
|
it("Q-24: renders context when present", () => {
|
package/src/component.ts
CHANGED
|
@@ -386,11 +386,15 @@ export class AskUserComponent implements Component {
|
|
|
386
386
|
// Terminals deliver pasted text as a single data chunk; iterating each char
|
|
387
387
|
// ensures the full paste is captured instead of silently dropping everything
|
|
388
388
|
// after the first character.
|
|
389
|
+
// 先剥离 bracketed paste 标记序列:启用该模式的终端会把粘贴内容包裹在
|
|
390
|
+
// \x1b[200~ ... \x1b[201~ 中。下面的 `c >= " "` 守卫会滤掉 ESC(\x1b),
|
|
391
|
+
// 但序列里的可见字符([200~/[201~)会残留混进编辑器文本,必须显式剥离。
|
|
392
|
+
const cleaned = data.replace(/\x1b\[200~|\x1b\[201~/g, "");
|
|
389
393
|
// for...of 按 code point 迭代:代理对(如 😀 U+1F600)作为一个 c(length===2)出现,
|
|
390
394
|
// 因此不能用 `c.length === 1` 守卫——那会把所有 BMP 之外的字符(emoji、部分 CJK 扩展)全过滤。
|
|
391
395
|
// 只保留 `c >= " "`(code point 比较),过滤控制字符(< 空格 U+0020)。
|
|
392
396
|
let changed = false;
|
|
393
|
-
for (const c of
|
|
397
|
+
for (const c of cleaned) {
|
|
394
398
|
if (c >= " ") {
|
|
395
399
|
this.editorText += c;
|
|
396
400
|
changed = true;
|
package/src/question-view.ts
CHANGED
|
@@ -106,25 +106,33 @@ function buildOptionLines(
|
|
|
106
106
|
const prefix = isSelected ? t.fg("accent", ">") : " ";
|
|
107
107
|
|
|
108
108
|
if (isOther) {
|
|
109
|
+
// 标记位宽度必须与普通选项一致,否则编号列错位:
|
|
110
|
+
// 单选 check = 1 列,多选 box = 3 列。
|
|
111
|
+
// 此前单选 freeform 占位用 " "(2列)、多选非 freeform 用 check(1列),
|
|
112
|
+
// 两种情况下 Other 编号都与普通选项错位。
|
|
109
113
|
if (state.mode === "freeform") {
|
|
110
|
-
|
|
111
|
-
// lead = "> [ ] ",编号 + 输入文本跟在框后,内容起始列与普通选项一致。
|
|
112
|
-
const box = q.multiSelect ? t.fg("dim", "[ ]") : " ";
|
|
114
|
+
const marker = q.multiSelect ? t.fg("dim", "[ ]") : " ";
|
|
113
115
|
const num = i + 1;
|
|
114
|
-
const lead = `${prefix} ${
|
|
116
|
+
const lead = `${prefix} ${marker} `;
|
|
115
117
|
const avail = Math.max(1, width - visibleWidth(lead));
|
|
116
118
|
// 编号 + 文本 + 末尾光标 █ 整体软换行(空 input 时仅编号 + 光标,wrapTextWithAnsi 单行)
|
|
117
119
|
const styled = `${t.fg("muted", `${num}. `)}${t.fg("text", editorText)}${t.fg("accent", "█")}`;
|
|
118
120
|
addWrappedInput(add, lead, styled, avail, MAX_EDITOR_LINES);
|
|
119
121
|
} else {
|
|
120
122
|
const hasFreeText = state.freeTextValue !== null;
|
|
121
|
-
const
|
|
123
|
+
const marker = q.multiSelect
|
|
124
|
+
? (hasFreeText ? t.fg("success", "[✓]") : t.fg("dim", "[ ]"))
|
|
125
|
+
: (hasFreeText ? t.fg("success", "✓") : " ");
|
|
122
126
|
const labelColor = isSelected ? "accent" : "muted";
|
|
123
127
|
const num = i + 1;
|
|
124
|
-
add(`${prefix} ${
|
|
128
|
+
add(`${prefix} ${marker} ${t.fg(labelColor, `${num}. ${opt.label}`)}`);
|
|
125
129
|
if (hasFreeText) {
|
|
126
|
-
//
|
|
127
|
-
|
|
130
|
+
// 预览缩进对齐到 label 起始列:prefix + sp + marker + sp + "N." + sp。
|
|
131
|
+
// 随 num 位数与单/多选 marker 宽度动态变化,硬编码会错位。
|
|
132
|
+
const numStr = `${num}.`;
|
|
133
|
+
const lead = " ".repeat(
|
|
134
|
+
visibleWidth(prefix) + 1 + visibleWidth(marker) + 1 + numStr.length + 1,
|
|
135
|
+
);
|
|
128
136
|
const avail = Math.max(1, width - visibleWidth(lead));
|
|
129
137
|
const styled = t.fg("dim", `"${state.freeTextValue ?? ""}"`);
|
|
130
138
|
addWrappedInput(add, lead, styled, avail, MAX_EDITOR_LINES);
|