From 5fa8628d6c86210a642f6ec95ad7dcbc732f4cda Mon Sep 17 00:00:00 2001 From: wswsadadbaba123 <790245083@qq.com> Date: Tue, 1 Sep 2026 23:01:27 +0800 Subject: [PATCH] fix the bug --- extensions/user-input-fold/index.ts | 48 +++++++++-- .../extensions/user-input-fold/index.test.ts | 85 ++++++++++++++++++- 2 files changed, 123 insertions(+), 10 deletions(-) diff --git a/extensions/user-input-fold/index.ts b/extensions/user-input-fold/index.ts index 4d1a6d46..f4f810c8 100644 --- a/extensions/user-input-fold/index.ts +++ b/extensions/user-input-fold/index.ts @@ -40,8 +40,35 @@ type Segment = | { kind: "prose"; lines: string[] } | { kind: "code"; open: string; content: string[]; close: string }; -const FENCE_OPEN = /^ {0,3}`{3,}/; -const FENCE_CLOSE = /^ {0,3}`{3,}[ \t]*\r?$/; +const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/; + +/** + * Parse an opening code fence (CommonMark §4.5): which character it uses and + * how long it is. A backtick fence's info string may not contain backticks. + */ +function openFence(line: string) { + const match = FENCE_OPEN.exec(line); + if (!match) return undefined; + const fence = match[1]; + if (fence[0] === "`" && line.slice(match[0].length).includes("`")) { + return undefined; + } + return { char: fence[0], length: fence.length }; +} + +/** + * A closing fence must use the same character as the opening fence and be at + * least as long: a ``` line does not close a ```` block, and backticks never + * close a tilde block. + */ +function isCloseFence(line: string, open: { char: string; length: number }) { + const match = /^ {0,3}(`{3,}|~{3,})[ \t]*\r?$/.exec(line); + return ( + match !== null && + match[1][0] === open.char && + match[1].length >= open.length + ); +} function countLines(markdown: string) { const parts = markdown.split("\n"); @@ -60,7 +87,8 @@ function parseSegments(lines: string[]): Segment[] { let prose: string[] = []; let i = 0; while (i < lines.length) { - if (!FENCE_OPEN.test(lines[i])) { + const fence = openFence(lines[i]); + if (!fence) { prose.push(lines[i]); i += 1; continue; @@ -74,13 +102,21 @@ function parseSegments(lines: string[]): Segment[] { let close: string | undefined; let j = i + 1; while (j < lines.length && close === undefined) { - if (FENCE_CLOSE.test(lines[j])) close = lines[j]; + if (isCloseFence(lines[j], fence)) close = lines[j]; else content.push(lines[j]); j += 1; } if (close === undefined) { - // Unterminated fence: fold the whole message conservatively as text. - return [{ kind: "prose", lines }]; + // Unterminated fence: the block runs to the end of the message. Keep it + // as a code block with a synthesized closing fence so a folded preview + // never leaks an unclosed fence into the TUI. + segments.push({ + kind: "code", + open, + content, + close: fence.char.repeat(fence.length), + }); + return segments; } segments.push({ kind: "code", open, content, close }); i = j; diff --git a/tests/extensions/user-input-fold/index.test.ts b/tests/extensions/user-input-fold/index.test.ts index 64bff6b1..94bdfffc 100644 --- a/tests/extensions/user-input-fold/index.test.ts +++ b/tests/extensions/user-input-fold/index.test.ts @@ -195,17 +195,94 @@ test("empty and whitespace-only messages are left alone", () => { } }); -test("an unterminated fence folds conservatively as plain text", () => { +test("an unterminated fence folds as a code block with a synthesized close", () => { const message = [ "```js", ...Array.from({ length: 30 }, (_, i) => `stmt ${i};`), ].join("\n"); const out = foldUserMessage(message); assert.ok(out.startsWith("```js")); - assert.ok(out.includes("stmt 10;")); - assert.ok(!out.includes("stmt 12;")); + assert.ok(out.includes("stmt 3;")); + assert.ok(!out.includes("stmt 4;")); + // The synthesized closing fence keeps the folded preview balanced Markdown. + assert.ok(out.includes("…\n```")); assert.ok( - out.endsWith("… folded 19 lines · full content was sent to the model"), + out.endsWith("… folded 25 lines · full content was sent to the model"), + ); +}); + +test("a nested ``` block does not close a ```` block (CommonMark §4.5)", () => { + const message = [ + "Please review this prompt template:", + "````markdown", + "Here is an example snippet:", + "```javascript", + "function hello() {", + ' console.log("Hello world");', + "}", + "```", + "Follow the instructions above carefully.", + ...Array.from({ length: 15 }, (_, i) => `${i + 1}. Step ${i + 1}`), + "````", + "End of message.", + ].join("\n"); + const out = foldUserMessage(message); + assert.ok( + out.startsWith("Please review this prompt template:\n````markdown"), + ); + // Content after the inner ``` fence stays inside the outer block. + assert.ok(!out.includes("Follow the instructions")); + assert.ok(!out.includes("Step 1")); + // The outer block is closed by its matching four-backtick fence. + assert.ok(out.includes("…\n````\nEnd of message.")); + assert.ok( + out.endsWith("… folded 18 lines · full content was sent to the model"), + ); +}); + +test("a longer closing fence closes a shorter opening fence", () => { + const message = ["~~~", ...numberedLines(30), "~~~~~~"].join("\n"); + const out = foldUserMessage(message); + assert.ok(out.startsWith("~~~\nline 01")); + assert.ok(out.includes("…\n~~~~~~")); + assert.ok( + out.endsWith("… folded 26 lines · full content was sent to the model"), + ); +}); + +test("tilde fences are recognized and fold like backtick fences", () => { + const message = [ + "intro", + "~~~py", + ...Array.from({ length: 30 }, (_, i) => `py-${i}`), + "~~~", + "outro", + ].join("\n"); + const out = foldUserMessage(message); + assert.ok(out.startsWith("intro\n~~~py")); + assert.ok(out.includes("py-3")); + assert.ok(!out.includes("py-4")); + assert.ok(out.includes("…\n~~~\noutro")); + assert.ok( + out.endsWith("… folded 26 lines · full content was sent to the model"), + ); +}); + +test("backtick and tilde fences never close each other", () => { + const message = [ + "~~~text", + "```", + ...numberedLines(30), + "```", + "~~~", + "tail", + ].join("\n"); + const out = foldUserMessage(message); + // The inner ``` lines are content of the tilde block, not its closer. + assert.ok(out.startsWith("~~~text\n```\nline 01")); + assert.ok(out.includes("…\n~~~\ntail")); + assert.ok( + out.endsWith("… folded 28 lines · full content was sent to the model"), ); });