feat(f9): splitBlocksWithRanges — block offsets aligned to the source

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-11 14:34:40 -07:00
parent 0eaca37d5e
commit 82d7c52983
2 changed files with 90 additions and 1 deletions
+70
View File
@@ -70,6 +70,76 @@ export function splitBlocks(text: string): Block[] {
return blocks;
}
export interface BlockWithRange extends Block {
/** char offset of the block's first char in the source string. */
start: number;
/** char offset one past the block's last char (source.slice(start,end) === raw). */
end: number;
}
/**
* Like splitBlocks, but each block carries its [start,end) char range in the
* SOURCE string (offsets align with document.getText(), so F3 attribution
* offsets map directly). Lines are tracked with their true source offsets — a
* trailing CR stays in the line, so a CRLF source keeps correct offsets.
*/
export function splitBlocksWithRanges(text: string): BlockWithRange[] {
const lines: { raw: string; start: number }[] = [];
let from = 0;
for (let i = 0; i <= text.length; i++) {
if (i === text.length || text[i] === "\n") {
lines.push({ raw: text.slice(from, i), start: from });
from = i + 1;
if (i === text.length) break;
}
}
const rangeOf = (lo: number, hi: number): { start: number; end: number } => ({
start: lines[lo].start,
end: lines[hi].start + lines[hi].raw.length,
});
const out: BlockWithRange[] = [];
let buf: number[] = [];
const flushProse = () => {
if (buf.length) {
const { start, end } = rangeOf(buf[0], buf[buf.length - 1]);
const raw = text.slice(start, end);
if (raw.trim()) out.push({ ...makeBlock(raw, "prose"), start, end });
}
buf = [];
};
let i = 0;
while (i < lines.length) {
const line = lines[i].raw;
const fence = line.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
if (fence) {
flushProse();
const marker = fence[2][0];
const info = fence[3].trim().split(/\s+/)[0].toLowerCase();
const open = i;
i++;
while (i < lines.length) {
const closed = lines[i].raw.trim().startsWith(marker.repeat(3));
i++;
if (closed) break;
}
const close = i - 1;
const { start, end } = rangeOf(open, close);
const raw = text.slice(start, end);
out.push({ ...makeBlock(raw, info === "mermaid" ? "mermaid" : "code"), start, end });
continue;
}
if (line.trim() === "") {
flushProse();
i++;
continue;
}
buf.push(i);
i++;
}
flushProse();
return out;
}
export type BlockOp =
| { kind: "unchanged"; block: Block }
| { kind: "added"; block: Block }