export function readInteger(value: unknown, fallback: number, maximum = Number.MAX_SAFE_INTEGER): number { const result = value ?? fallback; if (typeof result !== 'number' || !Number.isSafeInteger(result) || result < 1 || result > maximum) throw new Error('Invalid read range'); return result; } export function textChunks(text: string): string[] { return [text.replaceAll('\r\n', '\n')]; } // JSON contains contiguous original text; columns count Unicode code points, starting at 1. export async function readPage( source: string, chunks: AsyncIterable | Iterable, args: Record, maxBytes: number, ): Promise<{ content: string; truncated: boolean }> { const start = readInteger(args.start_line, 1); const column = readInteger(args.start_column, 1); const count = readInteger(args.line_count, 120, 400); const chars: string[] = []; let lineNumber = 1, position = 1, more = false; outer: for await (const chunk of chunks) { for (const char of chunk) { if (lineNumber === start && char === '\n' && position < column) throw new Error('Column is past the line'); if (lineNumber > start || (lineNumber === start && position >= column)) { if (lineNumber >= start + count || chars.length >= maxBytes) { more = true; break outer; } chars.push(char); } if (char === '\n') { lineNumber++; position = 1; } else position++; } } if (lineNumber < start || (lineNumber === start && position < column)) throw new Error('Read position is past the source'); const render = (length: number) => { let nextLine = start, nextColumn = column; for (let i = 0; i < length; i++) { if (chars[i] === '\n') { nextLine++; nextColumn = 1; } else nextColumn++; } const next = more || length < chars.length ? { start_line: nextLine, start_column: nextColumn } : null; return { content: JSON.stringify({ source, start_line: start, start_column: column, text: chars.slice(0, length).join(''), next }), truncated: next !== null }; }; const complete = render(chars.length); if (Buffer.byteLength(complete.content) <= maxBytes) return complete; let low = 0, high = chars.length; while (low < high) { const middle = Math.ceil((low + high) / 2); if (Buffer.byteLength(render(middle).content) <= maxBytes) low = middle; else high = middle - 1; } const page = render(low); if ((chars.length && !low) || Buffer.byteLength(page.content) > maxBytes) throw new Error('Read budget too small for a page'); return page; }