feat(f9): splitBlocksWithRanges — block offsets aligned to the source
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user