From 68850b0d63714b794ea9e10680e123b79c561d5d Mon Sep 17 00:00:00 2001 From: Vikram Bhamre Date: Tue, 11 Aug 2026 13:55:27 -0400 Subject: [PATCH] fix: quadratic code-block scan in isInsideCodeBlock --- .changeset/quadratic-code-block-scan.md | 5 ++ .../remend/__benchmarks__/remend.bench.ts | 17 ++++ .../remend/__tests__/code-block-utils.test.ts | 88 +++++++++++++++++++ packages/remend/src/code-block-utils.ts | 47 ++++++++-- 4 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 .changeset/quadratic-code-block-scan.md create mode 100644 packages/remend/__tests__/code-block-utils.test.ts diff --git a/.changeset/quadratic-code-block-scan.md b/.changeset/quadratic-code-block-scan.md new file mode 100644 index 00000000..72c1e559 --- /dev/null +++ b/.changeset/quadratic-code-block-scan.md @@ -0,0 +1,5 @@ +--- +"remend": patch +--- + +Fix quadratic code-block scanning. `isInsideCodeBlock` now builds a linear-time position lookup (cached per text) instead of rescanning the whole prefix on every call, so handlers that probe many positions no longer degrade quadratically. Repairing an unclosed, bracket-heavy 58k-character code block drops from ~915ms to ~0.4ms per call. diff --git a/packages/remend/__benchmarks__/remend.bench.ts b/packages/remend/__benchmarks__/remend.bench.ts index d8ac7c3b..98d206a6 100644 --- a/packages/remend/__benchmarks__/remend.bench.ts +++ b/packages/remend/__benchmarks__/remend.bench.ts @@ -279,3 +279,20 @@ ${"Regular paragraph text with some [links](https://example.com) and more conten { iterations: 1000 } ); }); + +describe("Streamed Code Blocks", () => { + // An unclosed fence full of brackets is the pathological case for the + // code-block scan: every "[" probes isInsideCodeBlock, which previously + // rescanned the whole prefix per probe (quadratic overall). + const bracketHeavyLine = + "const x = arr[i]; if (map[key]) { list[j] = grid[a][b]; }\n"; + const streamingCodeBlock = `\`\`\`ts\n${bracketHeavyLine.repeat(1000)}`; + + bench( + "unclosed bracket-heavy code block (58k chars)", + () => { + remend(streamingCodeBlock); + }, + { iterations: 10 } + ); +}); diff --git a/packages/remend/__tests__/code-block-utils.test.ts b/packages/remend/__tests__/code-block-utils.test.ts new file mode 100644 index 00000000..9bf45833 --- /dev/null +++ b/packages/remend/__tests__/code-block-utils.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; +import { isInsideCodeBlock } from "../src/code-block-utils"; + +// Reference implementation: the previous per-call scan, kept verbatim so the +// lookup-based rewrite can be checked against it position by position. +const referenceIsInsideCodeBlock = ( + text: string, + position: number +): boolean => { + let inInlineCode = false; + let inMultilineCode = false; + + for (let i = 0; i < position; i += 1) { + if (text[i] === "\\" && i + 1 < text.length && text[i + 1] === "`") { + i += 1; + continue; + } + if (text.substring(i, i + 3) === "```") { + inMultilineCode = !inMultilineCode; + i += 2; + continue; + } + if (!inMultilineCode && text[i] === "`") { + inInlineCode = !inInlineCode; + } + } + + return inInlineCode || inMultilineCode; +}; + +// Returns positions where the rewrite disagrees with the reference scan. +const parityMismatches = (text: string): number[] => { + const mismatches: number[] = []; + for (let p = 0; p <= text.length + 1; p += 1) { + if (isInsideCodeBlock(text, p) !== referenceIsInsideCodeBlock(text, p)) { + mismatches.push(p); + } + } + return mismatches; +}; + +describe("isInsideCodeBlock", () => { + it("reports positions inside a fenced code block", () => { + const text = "before ```js\nconst x = arr[0];\n``` after"; + expect(isInsideCodeBlock(text, text.indexOf("arr"))).toBe(true); + expect(isInsideCodeBlock(text, text.indexOf("before"))).toBe(false); + expect(isInsideCodeBlock(text, text.indexOf("after"))).toBe(false); + }); + + it("reports positions inside inline code", () => { + const text = "use `map[key]` here"; + expect(isInsideCodeBlock(text, text.indexOf("key"))).toBe(true); + expect(isInsideCodeBlock(text, text.indexOf("here"))).toBe(false); + }); + + it("ignores escaped backticks", () => { + const text = "not code \\` still [not] code"; + expect(isInsideCodeBlock(text, text.indexOf("[not]"))).toBe(false); + }); + + it("treats an unclosed fence as extending to the end", () => { + const text = "```python\nvalues[0] = 1"; + expect(isInsideCodeBlock(text, text.indexOf("values"))).toBe(true); + expect(isInsideCodeBlock(text, text.length)).toBe(true); + }); + + it("matches the per-call scan at every position on mixed input", () => { + const cases = [ + "a `b` c ```\nd [e] `f`\n``` g \\` h ``` i", + "``````", + "\\`", + "`unclosed inline [x]", + "text \\``real` code", + ]; + for (const text of cases) { + expect(parityMismatches(text)).toEqual([]); + } + }); + + it("stays correct when queried texts alternate", () => { + const inCode = "```\n[x]"; + const inProse = "plain [x]"; + for (let round = 0; round < 3; round += 1) { + expect(isInsideCodeBlock(inCode, inCode.indexOf("[x]"))).toBe(true); + expect(isInsideCodeBlock(inProse, inProse.indexOf("[x]"))).toBe(false); + } + }); +}); diff --git a/packages/remend/src/code-block-utils.ts b/packages/remend/src/code-block-utils.ts index ca3de4bd..b2ab6ad1 100644 --- a/packages/remend/src/code-block-utils.ts +++ b/packages/remend/src/code-block-utils.ts @@ -1,20 +1,34 @@ -// Check if a position is inside a code block (between ``` or `) -export const isInsideCodeBlock = (text: string, position: number): boolean => { - // Check for inline code (backticks) +// Builds the isInsideCodeBlock answer for every position in one linear pass. +// lookup[p] === 1 means scanning chars [0, p) ends inside inline or fenced +// code. Scanning per call is O(position), which makes callers that probe many +// positions (e.g. the link handler walking every "[" of a long streamed code +// block) quadratic overall. +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: "Mirrors the original scan's control flow exactly so the lookup is provably equivalent" +const buildCodeBlockLookup = (text: string): Uint8Array => { + const lookup = new Uint8Array(text.length + 1); let inInlineCode = false; let inMultilineCode = false; + let i = 0; - for (let i = 0; i < position; i += 1) { + while (i < text.length) { // Skip escaped backticks if (text[i] === "\\" && i + 1 < text.length && text[i + 1] === "`") { - i += 1; + const state = inInlineCode || inMultilineCode ? 1 : 0; + lookup[i + 1] = state; + lookup[i + 2] = state; + i += 2; continue; } // Check for triple backticks (multiline code blocks) if (text.substring(i, i + 3) === "```") { inMultilineCode = !inMultilineCode; - i += 2; // Skip the next 2 backticks + const state = inInlineCode || inMultilineCode ? 1 : 0; + const next = Math.min(i + 3, text.length); + for (let p = i + 1; p <= next; p += 1) { + lookup[p] = state; + } + i = next; continue; } @@ -22,9 +36,28 @@ export const isInsideCodeBlock = (text: string, position: number): boolean => { if (!inMultilineCode && text[i] === "`") { inInlineCode = !inInlineCode; } + lookup[i + 1] = inInlineCode || inMultilineCode ? 1 : 0; + i += 1; } - return inInlineCode || inMultilineCode; + return lookup; +}; + +// Handlers repeatedly probe positions of the same text within one remend() +// call, so a single-entry cache converts each probe to O(1) after one O(n) +// build per distinct text. +let cache: { text: string; lookup: Uint8Array } | null = null; + +// Check if a position is inside a code block (between ``` or `) +export const isInsideCodeBlock = (text: string, position: number): boolean => { + let current = cache; + if (current === null || current.text !== text) { + current = { text, lookup: buildCodeBlockLookup(text) }; + cache = current; + } + // Positions past the end resolve to the state after scanning the full text, + // matching the previous per-call scan. + return current.lookup[Math.min(position, text.length)] === 1; }; // Checks if a backtick at position i is part of a triple backtick sequence