Files
pi/packages/coding-agent/test/shell-embedded-code.test.ts
Armin Ronacher 3470327956 feat(coding-agent): highlight heredocs and inline scripts in bash calls
Heredoc bodies and interpreter inline scripts (python3 - <<'EOF', cat > file.rs <<'EOF', node -e '...') are split from the shell text and highlighted in their own language. The language comes from the interpreter behind wrappers such as uv run or timeout, the cat/tee target extension, or the heredoc delimiter. Long bodies collapse to 10 lines until expanded. PowerShell calls are unchanged.

Streaming highlight caching is shared with the write renderer: growing text only highlights new lines plus a bounded prefix, is fully highlighted once when the arguments complete, and is reused for output updates until the theme changes.
2026-09-24 23:00:19 +02:00

120 lines
5.5 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { type ShellSegment, splitShellCommand } from "../src/utils/shell-embedded-code.ts";
const EXTENSIONS: Record<string, string> = { rs: "rust", ts: "typescript", py: "python", json: "json" };
function split(command: string): ShellSegment[] {
const segments = splitShellCommand(command, {
languageFromPath: (path) => EXTENSIONS[path.slice(path.lastIndexOf(".") + 1)],
});
expect(segments.map((segment) => segment.text).join("")).toBe(command);
return segments;
}
function embedded(command: string): Array<{ text: string; language: string | undefined }> {
return split(command)
.filter((segment) => segment.embedded)
.map((segment) => ({ text: segment.text, language: segment.language }));
}
describe("splitShellCommand", () => {
test("leaves plain commands as a single shell segment", () => {
expect(split("cd repo && cargo test 2>&1 | grep -E 'a|b'")).toEqual([
{ text: "cd repo && cargo test 2>&1 | grep -E 'a|b'", embedded: false },
]);
});
test("splits a python heredoc and keeps following commands as shell", () => {
const command = "cd repo && python3 - <<'EOF'\ns = open('a').read()\nprint(\"it's\")\nEOF\ncargo test";
expect(split(command)).toEqual([
{ text: "cd repo && python3 - <<'EOF'\n", embedded: false },
{ text: "s = open('a').read()\nprint(\"it's\")\n", embedded: true, language: "python" },
{ text: "EOF\ncargo test", embedded: false },
]);
});
test("infers interpreters behind wrappers and redirects", () => {
expect(embedded("uv run --no-project --with httpx --with bs4 python - <<'PY'\nx = 1\nPY")).toEqual([
{ text: "x = 1\n", language: "python" },
]);
expect(embedded("./python.exe - <<'PY' > /tmp/log 2>&1\nx = 1\nPY")).toEqual([
{ text: "x = 1\n", language: "python" },
]);
expect(embedded("node --input-type=module <<'JS'\nconsole.log(1)\nJS")).toEqual([
{ text: "console.log(1)\n", language: "javascript" },
]);
expect(embedded("for d in a b; do timeout 20 bash <<'EOF'\necho $d\nEOF\ndone")).toEqual([
{ text: "echo $d\n", language: "bash" },
]);
});
test("uses the file extension for cat and tee targets", () => {
expect(embedded("cat >> deser-json/tests/test_de.rs <<'EOF'\n#[test]\nfn x() {}\nEOF")).toEqual([
{ text: "#[test]\nfn x() {}\n", language: "rust" },
]);
expect(embedded("cat <<'EOF' > out.json\n{}\nEOF")).toEqual([{ text: "{}\n", language: "json" }]);
expect(embedded("tee -a notes.py <<'EOF'\nx = 1\nEOF")).toEqual([{ text: "x = 1\n", language: "python" }]);
expect(embedded("F=test/zz-tmp-$$.test.ts && cat > $F <<'EOF'\nconst x = 1;\nEOF")).toEqual([
{ text: "const x = 1;\n", language: "typescript" },
]);
});
test("falls back to the delimiter name when stdin is data for a script", () => {
expect(embedded("node tools/workspace.js exec --email a@b.c <<'JS' >out.json &\nreturn 1;\nJS")).toEqual([
{ text: "return 1;\n", language: "javascript" },
]);
expect(embedded("python3 script.py <<'EOF'\ndata\nEOF")).toEqual([{ text: "data\n", language: undefined }]);
expect(embedded("git commit -F - <<'EOF'\nfix: thing\nEOF")).toEqual([
{ text: "fix: thing\n", language: undefined },
]);
});
test("handles <<- and consecutive heredocs", () => {
expect(embedded("python3 - <<-PY\n\tx = 1\n\tPY\nnode <<'JS'\ny()\nJS\n")).toEqual([
{ text: "\tx = 1\n", language: "python" },
{ text: "y()\n", language: "javascript" },
]);
});
test("treats an unterminated heredoc as running to the end while streaming", () => {
expect(split("python3 - <<'EOF'\nprint(1)\nEO")).toEqual([
{ text: "python3 - <<'EOF'\n", embedded: false },
{ text: "print(1)\nEO", embedded: true, language: "python" },
]);
expect(split("python3 - <<'EO")).toEqual([{ text: "python3 - <<'EO", embedded: false }]);
});
test("ignores << inside quotes, arithmetic, and here-strings", () => {
expect(embedded("rg -n 'a <<EOF b' && echo $((1 << 3)) && cat <<< \"x\"\nfoo")).toEqual([]);
});
test("extracts inline interpreter scripts", () => {
expect(split('cd x && python3 -c "\nimport sys\nprint(sys.argv)\n" 2>&1 | tail')).toEqual([
{ text: 'cd x && python3 -c "', embedded: false },
{ text: "\nimport sys\nprint(sys.argv)\n", embedded: true, language: "python" },
{ text: '" 2>&1 | tail', embedded: false },
]);
expect(embedded("node -e 'console.log(1)'")).toEqual([{ text: "console.log(1)", language: "javascript" }]);
expect(embedded("ruby -rjson -e 'puts 1'")).toEqual([{ text: "puts 1", language: "ruby" }]);
expect(embedded("perl -0pi -e 's/a/b/' file")).toEqual([{ text: "s/a/b/", language: "perl" }]);
expect(embedded("zsh -i -c 'echo hi'")).toEqual([{ text: "echo hi", language: "bash" }]);
expect(embedded("awk -F: '{ print $1 }' /etc/passwd")).toEqual([{ text: "{ print $1 }", language: "awk" }]);
expect(embedded("osascript -e 'tell app \"Finder\" to quit'")).toEqual([
{ text: 'tell app "Finder" to quit', language: "applescript" },
]);
});
test("does not treat flags of other tools as inline scripts", () => {
expect(embedded("grep -e 'foo' x && rg -n 'bar' && git commit -m \"feat: x\n\nbody\"")).toEqual([]);
expect(embedded("sed -e 's/a/b/' x && python3 script.py -c 'x'")).toEqual([]);
expect(embedded("python3 -m pytest -c 'conf.ini' && python -mpytest -c 'conf.ini'")).toEqual([]);
expect(embedded("python3 -m tool <<'EOF'\ndata\nEOF")).toEqual([{ text: "data\n", language: undefined }]);
});
test("finds heredocs inside command substitution", () => {
expect(embedded("git commit -m \"$(cat <<'EOF'\nfix: x\nEOF\n)\"")).toEqual([
{ text: "fix: x\n", language: undefined },
]);
});
});