Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions extensions/user-input-fold/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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;
Expand All @@ -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;
Expand Down
85 changes: 81 additions & 4 deletions tests/extensions/user-input-fold/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
);
});

Expand Down
Loading