// @generated vendored verbatim from oh-my-pi packages/hashline @ 15b5c1397fc -- DO NOT EDIT.
// Parity source for the Atomic hashline edit engine (issue #1483); adapted only for Atomic's Node runtime (relative imports, Bun->Node host calls, erasable constructor syntax).
/**
 * Apply a parsed list of {@link Edit}s to a text body and return the
 * post-edit lines plus any diagnostic warnings. Pure function: no FS, no
 * mutation of the input.
 *
 * Replacement groups are first normalized by {@link repairReplacementBoundaries},
 * which absorbs common model mistakes where a payload restates unchanged range
 * boundaries or duplicates/drops structural closers.
 */
import { afterInsertLandingShiftWarning, blockInsertLandingShiftWarning, UNRESOLVED_BLOCK_INTERNAL } from "./messages.js";
import { cloneCursor } from "./tokenizer.js";
function isReplacementInsert(edit) {
    return edit.kind === "insert" && edit.mode === "replacement";
}
function getCursorAnchors(cursor) {
    return cursor.kind === "before_anchor" || cursor.kind === "after_anchor" ? [cursor.anchor] : [];
}
function getEditAnchors(edit) {
    if (edit.kind === "delete")
        return [edit.anchor];
    return getCursorAnchors(edit.cursor);
}
function trailingPhantomLine(fileLines) {
    // `split("\n")` on a newline-terminated file yields a trailing "" sentinel.
    // It is addressable for inserts (append-past-end), but it is not real
    // content. Deleting it only strips the file's final newline, so ignore delete
    // edits that land there; inclusive ranges ending at EOF then do the intended
    // thing and delete through the last concrete line.
    return fileLines.length > 1 && fileLines[fileLines.length - 1] === "" ? fileLines.length : 0;
}
function dropTrailingPhantomDeletes(edits, fileLines) {
    const phantomLine = trailingPhantomLine(fileLines);
    if (phantomLine === 0)
        return edits;
    return edits.filter(edit => edit.kind !== "delete" || edit.anchor.line !== phantomLine);
}
/**
 * Verify every anchored edit points at an existing line. File-version binding is
 * checked once per section via the header hash before this function runs.
 */
function validateLineBounds(edits, fileLines) {
    for (const edit of edits) {
        for (const anchor of getEditAnchors(edit)) {
            if (anchor.line < 1 || anchor.line > fileLines.length) {
                throw new Error(`Line ${anchor.line} does not exist (file has ${fileLines.length} lines)`);
            }
        }
    }
}
function cloneAppliedEdit(edit, index) {
    if (edit.kind === "delete")
        return { ...edit, anchor: { ...edit.anchor }, index };
    return { ...edit, cursor: cloneCursor(edit.cursor), index };
}
function insertAtStart(fileLines, lineOrigins, lines) {
    if (lines.length === 0)
        return;
    const origins = lines.map(() => "insert");
    if (fileLines.length === 1 && fileLines[0] === "") {
        fileLines.splice(0, 1, ...lines);
        lineOrigins.splice(0, 1, ...origins);
        return;
    }
    fileLines.splice(0, 0, ...lines);
    lineOrigins.splice(0, 0, ...origins);
}
function insertAtEnd(fileLines, lineOrigins, lines) {
    if (lines.length === 0)
        return undefined;
    const origins = lines.map(() => "insert");
    if (fileLines.length === 1 && fileLines[0] === "") {
        fileLines.splice(0, 1, ...lines);
        lineOrigins.splice(0, 1, ...origins);
        return 1;
    }
    const hasTrailingNewline = fileLines.length > 0 && fileLines[fileLines.length - 1] === "";
    const insertIndex = hasTrailingNewline ? fileLines.length - 1 : fileLines.length;
    fileLines.splice(insertIndex, 0, ...lines);
    lineOrigins.splice(insertIndex, 0, ...origins);
    return insertIndex + 1;
}
function bucketAnchorEditsByLine(edits) {
    const byLine = new Map();
    for (const entry of edits) {
        const line = entry.edit.kind === "delete"
            ? entry.edit.anchor.line
            : entry.edit.cursor.kind === "before_anchor" || entry.edit.cursor.kind === "after_anchor"
                ? entry.edit.cursor.anchor.line
                : 0;
        const bucket = byLine.get(line);
        if (bucket)
            bucket.push(entry);
        else
            byLine.set(line, [entry]);
    }
    return byLine;
}
// ═══════════════════════════════════════════════════════════════════════════
// Replacement-boundary repair
//
// Models routinely miscount a replacement range's edges. Sometimes the payload
// re-states unchanged lines that still live on both sides of the range
// (duplicating a function header and final statement); sometimes it only
// re-states or omits a structural closer, which leaves delimiter balance broken.
//
// A balance-neutral boundary-echo repair fires only when both the leading and
// trailing payload edges are exact copies of the surviving lines outside the
// range. One-sided content echoes are left alone unless delimiter-balance repair
// proves they are duplicated structural boundaries. This preserves intended
// duplicate statements while absorbing the common "body includes the unchanged
// wrapper" mistake.
/** A line that is nothing but closing delimiters: `}`, `)`, `];`, `})`, `},`. */
export const STRUCTURAL_CLOSER_RE = /^\s*[)\]}]+[;,]?\s*$/;
/**
 * Net `()` / `[]` / `{}` delta across `lines`, skipping delimiters inside line
 * comments (`//`), block comments, and string/template literals. Block-comment
 * and backtick-template state carry across lines; `"` / `'` reset at EOL since
 * they cannot span lines. Deliberately language-light: constructs it cannot
 * classify (e.g. regex literals) are counted naively, which can only suppress a
 * repair (the safe direction), never force one.
 */
function computeDelimiterBalance(lines) {
    const balance = { paren: 0, bracket: 0, brace: 0 };
    let inBlockComment = false;
    let quote = "";
    for (const line of lines) {
        for (let i = 0; i < line.length; i++) {
            const ch = line[i];
            if (inBlockComment) {
                if (ch === "*" && line[i + 1] === "/") {
                    inBlockComment = false;
                    i++;
                }
                continue;
            }
            if (quote) {
                if (ch === "\\")
                    i++;
                else if (ch === quote)
                    quote = "";
                continue;
            }
            if (ch === '"' || ch === "'" || ch === "`") {
                quote = ch;
                continue;
            }
            if (ch === "/" && line[i + 1] === "/")
                break;
            if (ch === "/" && line[i + 1] === "*") {
                inBlockComment = true;
                i++;
                continue;
            }
            switch (ch) {
                case "(":
                    balance.paren++;
                    break;
                case ")":
                    balance.paren--;
                    break;
                case "[":
                    balance.bracket++;
                    break;
                case "]":
                    balance.bracket--;
                    break;
                case "{":
                    balance.brace++;
                    break;
                case "}":
                    balance.brace--;
                    break;
            }
        }
        // `"` / `'` cannot span lines; only backtick templates and block comments do.
        if (quote === '"' || quote === "'")
            quote = "";
    }
    return balance;
}
function balanceDelta(a, b) {
    return { paren: a.paren - b.paren, bracket: a.bracket - b.bracket, brace: a.brace - b.brace };
}
function balanceNegate(a) {
    return { paren: -a.paren, bracket: -a.bracket, brace: -a.brace };
}
function balanceEqual(a, b) {
    return a.paren === b.paren && a.bracket === b.bracket && a.brace === b.brace;
}
function balanceIsZero(a) {
    return a.paren === 0 && a.bracket === 0 && a.brace === 0;
}
/**
 * Detect a replacement group starting at `start`: a run of `before_anchor`
 * replacement inserts sharing one source op line, immediately followed by the
 * contiguous range deletes for that same op. Mirrors how the parser lowers an
 * `replace N..M:` hunk with a body.
 */
function findReplacementGroup(edits, start) {
    const first = edits[start];
    if (first?.kind !== "insert" || first.mode !== "replacement" || first.cursor.kind !== "before_anchor") {
        return undefined;
    }
    const { lineNum } = first;
    const anchorLine = first.cursor.anchor.line;
    const insertIndices = [];
    const payload = [];
    let i = start;
    for (; i < edits.length; i++) {
        const edit = edits[i];
        if (edit.kind !== "insert" || edit.mode !== "replacement" || edit.lineNum !== lineNum)
            break;
        if (edit.cursor.kind !== "before_anchor" || edit.cursor.anchor.line !== anchorLine)
            break;
        insertIndices.push(i);
        payload.push(edit.text);
    }
    const deleteIndices = [];
    let expectedLine = anchorLine;
    for (; i < edits.length; i++) {
        const edit = edits[i];
        if (edit.kind !== "delete" || edit.lineNum !== lineNum || edit.anchor.line !== expectedLine)
            break;
        deleteIndices.push(i);
        expectedLine++;
    }
    if (deleteIndices.length === 0)
        return undefined;
    return {
        insertIndices,
        deleteIndices,
        payload,
        startLine: anchorLine,
        endLine: anchorLine + deleteIndices.length - 1,
    };
}
/**
 * Largest `k` such that the payload's last `k` lines exactly equal the `k`
 * surviving file lines just below the range AND dropping them zeroes `delta`.
 * Requires a non-zero `delta`: a zero-balance candidate can never account for
 * the imbalance, so intentional duplicates of ordinary statements stay intact,
 * while duplicated structural lines (closers like `});`, openers like `foo(`)
 * are dropped when they exactly explain the imbalance.
 */
function findDuplicateSuffix(group, fileLines, delta) {
    if (balanceIsZero(delta))
        return 0;
    const { payload, endLine } = group;
    const maxK = Math.min(payload.length, fileLines.length - endLine);
    for (let k = maxK; k >= 1; k--) {
        let matches = true;
        for (let t = 0; t < k; t++) {
            if (payload[payload.length - k + t] !== fileLines[endLine + t]) {
                matches = false;
                break;
            }
        }
        if (!matches)
            continue;
        if (balanceEqual(computeDelimiterBalance(payload.slice(payload.length - k)), delta))
            return k;
    }
    return 0;
}
/**
 * Largest `j` such that the payload's first `j` lines exactly equal the `j`
 * surviving file lines just above the range AND dropping them zeroes `delta`.
 * Requires a non-zero `delta`; see {@link findDuplicateSuffix}.
 */
function findDuplicatePrefix(group, fileLines, delta) {
    if (balanceIsZero(delta))
        return 0;
    const { payload, startLine } = group;
    const maxJ = Math.min(payload.length, startLine - 1);
    for (let j = maxJ; j >= 1; j--) {
        let matches = true;
        for (let t = 0; t < j; t++) {
            if (payload[t] !== fileLines[startLine - 1 - j + t]) {
                matches = false;
                break;
            }
        }
        if (!matches)
            continue;
        if (balanceEqual(computeDelimiterBalance(payload.slice(0, j)), delta))
            return j;
    }
    return 0;
}
/**
 * Smallest `m` such that the range's last `m` deleted lines are all pure
 * structural closers and sparing them (keeping instead of deleting) zeroes
 * `delta`. The mirror mistake: a range that swallows a closing delimiter the
 * payload never restates.
 */
function findDroppedSuffixClosers(group, fileLines, delta) {
    const wanted = balanceNegate(delta);
    const maxM = group.deleteIndices.length;
    for (let m = 1; m <= maxM; m++) {
        if (!STRUCTURAL_CLOSER_RE.test(fileLines[group.endLine - m] ?? ""))
            break;
        if (balanceEqual(computeDelimiterBalance(fileLines.slice(group.endLine - m, group.endLine)), wanted))
            return m;
    }
    return 0;
}
function hasNonWhitespace(text) {
    for (let i = 0; i < text.length; i++) {
        const code = text.charCodeAt(i);
        if (code !== 9 && code !== 10 && code !== 11 && code !== 12 && code !== 13 && code !== 32)
            return true;
    }
    return false;
}
function countDuplicateLeadingBoundaryLines(group, fileLines) {
    const { payload, startLine } = group;
    const max = Math.min(payload.length, startLine - 1);
    for (let count = max; count >= 1; count--) {
        let matches = true;
        let hasContent = false;
        for (let offset = 0; offset < count; offset++) {
            const line = payload[offset];
            if (line !== fileLines[startLine - 1 - count + offset]) {
                matches = false;
                break;
            }
            hasContent ||= hasNonWhitespace(line);
        }
        if (matches && hasContent)
            return count;
    }
    return 0;
}
function countDuplicateTrailingBoundaryLines(group, fileLines) {
    const { payload, endLine } = group;
    const max = Math.min(payload.length, fileLines.length - endLine);
    for (let count = max; count >= 1; count--) {
        let matches = true;
        let hasContent = false;
        for (let offset = 0; offset < count; offset++) {
            const line = payload[payload.length - count + offset];
            if (line !== fileLines[endLine + offset]) {
                matches = false;
                break;
            }
            hasContent ||= hasNonWhitespace(line);
        }
        if (matches && hasContent)
            return count;
    }
    return 0;
}
function findBoundaryEcho(group, fileLines) {
    const leadingMax = countDuplicateLeadingBoundaryLines(group, fileLines);
    if (leadingMax === 0)
        return undefined;
    const trailingMax = countDuplicateTrailingBoundaryLines(group, fileLines);
    if (trailingMax === 0)
        return undefined;
    // Bail when every payload line could be claimed by a boundary echo: any
    // repair would strip explicit replacement content with no signal that the
    // payload was a mistake rather than an intentional duplication.
    if (leadingMax + trailingMax >= group.payload.length)
        return undefined;
    // Balance-neutrality guard (see header comment): the dropped echo lines must
    // either be delimiter-neutral on their own or exactly cancel the payload/range
    // balance delta. In brace-heavy code where bare closer lines repeat, an
    // "echo" that shifts delimiter balance is structural content the payload
    // placed intentionally — stripping it would corrupt the result.
    const leadingBalance = computeDelimiterBalance(group.payload.slice(0, leadingMax));
    const trailingBalance = computeDelimiterBalance(group.payload.slice(group.payload.length - trailingMax));
    const droppedBalance = balanceDelta(leadingBalance, balanceNegate(trailingBalance));
    if (!balanceIsZero(droppedBalance)) {
        const delta = balanceDelta(computeDelimiterBalance(group.payload), computeDelimiterBalance(fileLines.slice(group.startLine - 1, group.endLine)));
        if (!balanceEqual(droppedBalance, delta))
            return undefined;
    }
    return { leading: leadingMax, trailing: trailingMax };
}
function describeBoundaryEchoRepair(group, echo) {
    return (`Auto-repaired a replacement boundary echo at line ${group.startLine}: ` +
        `dropped ${echo.leading} leading and ${echo.trailing} trailing payload line(s) already present outside the range. ` +
        `Issue the payload as the final desired content for the selected range only — never restate unchanged lines bordering the range.`);
}
function describeBoundaryRepair(group, action) {
    return (`Auto-repaired a delimiter-balance mismatch in the replacement at line ${group.startLine}: ${action}. ` +
        `Issue the payload as the final desired content only — never restate or omit a closing bracket bordering the range.`);
}
/**
 * Normalize replacement groups so common off-by-one boundaries do not duplicate
 * unchanged surrounding lines or structural closers. Returns the repaired edit
 * list plus one warning per repaired group.
 */
function repairReplacementBoundaries(edits, fileLines) {
    const out = [];
    const warnings = [];
    let i = 0;
    while (i < edits.length) {
        const group = findReplacementGroup(edits, i);
        if (!group) {
            out.push(edits[i]);
            i++;
            continue;
        }
        const inserts = group.insertIndices.map(idx => edits[idx]);
        const deletes = group.deleteIndices.map(idx => edits[idx]);
        i = group.deleteIndices[group.deleteIndices.length - 1] + 1;
        const boundaryEcho = findBoundaryEcho(group, fileLines);
        if (boundaryEcho) {
            warnings.push(describeBoundaryEchoRepair(group, boundaryEcho));
            out.push(...inserts.slice(boundaryEcho.leading, inserts.length - boundaryEcho.trailing), ...deletes);
            continue;
        }
        const delta = balanceDelta(computeDelimiterBalance(group.payload), computeDelimiterBalance(fileLines.slice(group.startLine - 1, group.endLine)));
        if (balanceIsZero(delta)) {
            out.push(...inserts, ...deletes);
            continue;
        }
        const dupSuffix = findDuplicateSuffix(group, fileLines, delta);
        if (dupSuffix > 0) {
            warnings.push(describeBoundaryRepair(group, `dropped ${dupSuffix} duplicated trailing payload line(s) already present below the range`));
            out.push(...inserts.slice(0, inserts.length - dupSuffix), ...deletes);
            continue;
        }
        const dupPrefix = findDuplicatePrefix(group, fileLines, delta);
        if (dupPrefix > 0) {
            warnings.push(describeBoundaryRepair(group, `dropped ${dupPrefix} duplicated leading payload line(s) already present above the range`));
            out.push(...inserts.slice(dupPrefix), ...deletes);
            continue;
        }
        const droppedClosers = findDroppedSuffixClosers(group, fileLines, delta);
        if (droppedClosers > 0) {
            warnings.push(describeBoundaryRepair(group, `kept ${droppedClosers} structural closing line(s) the range deleted without restating`));
            out.push(...inserts, ...deletes.slice(0, deletes.length - droppedClosers));
            continue;
        }
        out.push(...inserts, ...deletes);
    }
    return { edits: out, warnings };
}
// ═══════════════════════════════════════════════════════════════════════════
// After-insert landing correction
//
// The body rows of an `insert after N:` hunk carry an implicit depth claim:
// their leading indentation says how deep the author expects the new lines
// to sit. Two corrections share that claim, in opposite directions:
//
// Outward (any after-insert): when the depth is shallower than line N itself,
// the hunk is inserting a sibling of some enclosing construct while anchored
// inside it — the common shape is anchoring on the last statement of a block
// and writing the body at the parent's depth. Sliding the landing point
// forward across the structural closer lines that follow (and nothing else —
// content lines are never crossed) places the body at the depth its
// indentation names.
//
// Inward (block-lowered inserts only): `insert after block N:` anchors on the
// resolved block's closing line, but a body indented deeper than that closer
// claims a depth inside the block — the common misreading of the op as
// "append at the end of block N's body". Sliding the landing point backward
// across the block's trailing closer lines places the body inside, at its
// claimed depth. Scoped to block-lowered inserts because there the author
// named the opener and never saw the closer; a plain `insert after M:` on a
// closer line stays literal (the escape hatch for genuinely-after content
// such as method-chain continuations).
//
// Both shifts are deliberately conservative: they fire only when the body
// and anchor indentation are comparable (one is a prefix of the other),
// cross only pure closing-delimiter lines, stop as soon as depth matches the
// body's claim, and are abandoned when any other edit in the patch targets a
// crossed line. Every shift is reported as a warning so the author can
// re-issue when the original landing was intended.
/** Leading run of tabs and spaces. */
function leadingIndent(line) {
    let end = 0;
    while (end < line.length) {
        const code = line.charCodeAt(end);
        if (code !== 9 && code !== 32)
            break;
        end++;
    }
    return line.slice(0, end);
}
/** `deeper` strictly extends `shallower` (same indent style, more depth). */
function isIndentDeeper(deeper, shallower) {
    return deeper.length > shallower.length && deeper.startsWith(shallower);
}
/**
 * Depth of an after-insert hunk's body: the shallowest indentation across its
 * non-blank rows. Returns `undefined` when no depth claim can be made — an
 * all-blank or all-closer body, or rows whose indentation styles are not
 * mutually comparable (tabs vs spaces).
 */
function bodyTargetIndent(rows) {
    const nonBlank = rows.filter(hasNonWhitespace);
    if (nonBlank.length === 0)
        return undefined;
    // A body of pure closers re-balances delimiters; it claims no depth.
    if (nonBlank.every(row => STRUCTURAL_CLOSER_RE.test(row)))
        return undefined;
    let target = leadingIndent(nonBlank[0] ?? "");
    for (const row of nonBlank) {
        const indent = leadingIndent(row);
        if (indent.startsWith(target))
            continue;
        if (target.startsWith(indent))
            target = indent;
        else
            return undefined;
    }
    return target;
}
/**
 * Resolve where an after-insert hunk anchored on `group.anchor` should land
 * given its body depth `target`: the last structural closer line in the run
 * directly below the anchor whose indentation still covers `target`. Returns
 * `undefined` when the landing stays put.
 */
function resolveShiftedLanding(group, target, fileLines, targetedLines) {
    const anchorText = fileLines[group.anchor - 1];
    if (anchorText === undefined || !hasNonWhitespace(anchorText))
        return undefined;
    if (!isIndentDeeper(leadingIndent(anchorText), target))
        return undefined;
    let landing = group.anchor;
    let crossed = 0;
    for (let line = group.anchor + 1; line <= fileLines.length; line++) {
        const text = fileLines[line - 1] ?? "";
        if (!hasNonWhitespace(text))
            continue; // look past blanks, never land on them
        if (!STRUCTURAL_CLOSER_RE.test(text))
            break; // content is never crossed
        const indent = leadingIndent(text);
        if (!indent.startsWith(target))
            break; // shallower than the body — crossing would over-escape
        if (targetedLines.has(line))
            return undefined; // another hunk owns this closer
        landing = line;
        crossed++;
        if (indent.length === target.length)
            break; // depth returned to the body's level
    }
    return landing === group.anchor ? undefined : { line: landing, crossed };
}
/**
 * Resolve where a block-lowered after-insert anchored on the block's closing
 * line should land given a body depth `target` deeper than that closer: just
 * above the block's trailing run of closer lines, bounded below by
 * `blockStart` (an empty block lands the body right after its opener).
 * Returns `undefined` when the landing stays put.
 */
function resolveInwardLanding(group, target, blockStart, fileLines, targetedLines) {
    const anchorText = fileLines[group.anchor - 1];
    if (anchorText === undefined || !hasNonWhitespace(anchorText))
        return undefined;
    // Fires only when the block ends in a pure closer the body out-indents.
    // Blocks ending in content (indentation-only languages) already land the
    // body inside the block — nothing to correct.
    if (!STRUCTURAL_CLOSER_RE.test(anchorText))
        return undefined;
    if (!isIndentDeeper(target, leadingIndent(anchorText)))
        return undefined;
    let landing = group.anchor;
    for (let line = group.anchor; line > blockStart; line--) {
        const text = fileLines[line - 1] ?? "";
        if (!hasNonWhitespace(text)) {
            landing = line - 1; // look past trailing blanks, never land after one
            continue;
        }
        if (!STRUCTURAL_CLOSER_RE.test(text))
            break; // content reached — land right after it
        const indent = leadingIndent(text);
        if (!isIndentDeeper(target, indent))
            break; // closer at the body's depth — land after it
        // Another hunk owns this closer (the group's own rows put the anchor
        // itself in `targetedLines`; that one is ours to cross).
        if (line !== group.anchor && targetedLines.has(line))
            return undefined;
        landing = line - 1;
    }
    return landing === group.anchor ? undefined : landing;
}
/**
 * Slide mis-anchored after-insert hunks to the depth their body indentation
 * claims: outward past the structural closer lines that follow the anchor
 * when the body is shallower, or — for `insert after block N:` lowerings —
 * inward across the block's trailing closers when the body is deeper than
 * the block's closing line. Returns the corrected edit list plus one warning
 * per shifted hunk.
 */
function repairAfterInsertLandings(edits, fileLines) {
    // Group plain (non-replacement) after-anchor inserts per authored hunk:
    // rows of one hunk share the anchor line and the patch header line.
    const groups = new Map();
    edits.forEach((edit, idx) => {
        if (edit.kind !== "insert" || edit.mode === "replacement")
            return;
        if (edit.cursor.kind !== "after_anchor")
            return;
        const key = `${edit.cursor.anchor.line}:${edit.lineNum}`;
        const group = groups.get(key);
        if (group === undefined)
            groups.set(key, { anchor: edit.cursor.anchor.line, members: [idx], blockStart: edit.blockStart });
        else
            group.members.push(idx);
    });
    if (groups.size === 0)
        return { edits, warnings: [] };
    // Lines explicitly targeted by any edit; a shift never crosses them.
    const targetedLines = new Set();
    for (const edit of edits) {
        if (edit.kind === "delete")
            targetedLines.add(edit.anchor.line);
        else if (edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor")
            targetedLines.add(edit.cursor.anchor.line);
    }
    let out;
    const warnings = [];
    const retarget = (group, line) => {
        out ??= [...edits];
        for (const idx of group.members) {
            const edit = out[idx];
            out[idx] = { ...edit, cursor: { kind: "after_anchor", anchor: { line } } };
        }
    };
    for (const group of groups.values()) {
        const target = bodyTargetIndent(group.members.map(idx => edits[idx].text));
        if (target === undefined)
            continue;
        const outward = resolveShiftedLanding(group, target, fileLines, targetedLines);
        if (outward !== undefined) {
            retarget(group, outward.line);
            warnings.push(afterInsertLandingShiftWarning(group.anchor, outward.line, outward.crossed));
            continue;
        }
        if (group.blockStart === undefined)
            continue;
        const inward = resolveInwardLanding(group, target, group.blockStart, fileLines, targetedLines);
        if (inward === undefined)
            continue;
        retarget(group, inward);
        warnings.push(blockInsertLandingShiftWarning(group.blockStart, group.anchor, inward));
    }
    return { edits: out ?? edits, warnings };
}
/**
 * Apply a parsed list of edits to a text body. Pure function — no I/O.
 *
 * Returns the post-edit text and the first changed line number (1-indexed).
 * Throws if an anchor is out of bounds.
 */
export function applyEdits(text, edits) {
    if (edits.length === 0)
        return { text, firstChangedLine: undefined };
    // Block edits are deferred until `resolveBlockEdits` expands them into
    // concrete inserts + deletes. Reaching the applier with one still present
    // is an internal wiring bug, not authored-input error.
    for (const edit of edits) {
        if (edit.kind === "block")
            throw new Error(UNRESOLVED_BLOCK_INTERNAL);
    }
    const appliedEdits = edits;
    const fileLines = text.split("\n");
    const lineOrigins = fileLines.map(() => "original");
    let firstChangedLine;
    const trackFirstChanged = (line) => {
        if (firstChangedLine === undefined || line < firstChangedLine)
            firstChangedLine = line;
    };
    const targetEdits = dropTrailingPhantomDeletes(appliedEdits.map((edit, index) => cloneAppliedEdit(edit, index)), fileLines);
    validateLineBounds(targetEdits, fileLines);
    const { edits: repaired, warnings: boundaryWarnings } = repairReplacementBoundaries(targetEdits, fileLines);
    const { edits: landed, warnings: landingWarnings } = repairAfterInsertLandings(repaired, fileLines);
    const warnings = [...boundaryWarnings, ...landingWarnings];
    // Partition edits into bof, eof, and anchor-targeted buckets.
    const bofLines = [];
    const eofLines = [];
    const anchorEdits = [];
    landed.forEach((edit, idx) => {
        if (edit.kind === "insert" && edit.cursor.kind === "bof") {
            bofLines.push(edit.text);
        }
        else if (edit.kind === "insert" && edit.cursor.kind === "eof") {
            eofLines.push(edit.text);
        }
        else {
            anchorEdits.push({ edit, idx });
        }
    });
    // Apply per-line buckets bottom-up so earlier indices stay valid.
    const byLine = bucketAnchorEditsByLine(anchorEdits);
    for (const line of [...byLine.keys()].sort((a, b) => b - a)) {
        const bucket = byLine.get(line);
        if (!bucket)
            continue;
        bucket.sort((a, b) => a.idx - b.idx);
        const idx = line - 1;
        const currentLine = fileLines[idx] ?? "";
        const beforeInsertLines = [];
        const afterInsertLines = [];
        const replacementLines = [];
        let deleteLine = false;
        for (const { edit } of bucket) {
            if (isReplacementInsert(edit)) {
                replacementLines.push(edit.text);
            }
            else if (edit.kind === "insert" && edit.cursor.kind === "after_anchor") {
                afterInsertLines.push(edit.text);
            }
            else if (edit.kind === "insert") {
                beforeInsertLines.push(edit.text);
            }
            else if (edit.kind === "delete") {
                deleteLine = true;
            }
        }
        if (beforeInsertLines.length === 0 &&
            replacementLines.length === 0 &&
            afterInsertLines.length === 0 &&
            !deleteLine)
            continue;
        const replacement = deleteLine
            ? [...beforeInsertLines, ...replacementLines, ...afterInsertLines]
            : [...beforeInsertLines, ...replacementLines, currentLine, ...afterInsertLines];
        const origins = [];
        for (let i = 0; i < beforeInsertLines.length; i++)
            origins.push("insert");
        for (let i = 0; i < replacementLines.length; i++)
            origins.push(deleteLine ? "replacement" : "insert");
        if (!deleteLine)
            origins.push(lineOrigins[idx] ?? "original");
        for (let i = 0; i < afterInsertLines.length; i++)
            origins.push("insert");
        fileLines.splice(idx, 1, ...replacement);
        lineOrigins.splice(idx, 1, ...origins);
        trackFirstChanged(line);
    }
    if (bofLines.length > 0) {
        insertAtStart(fileLines, lineOrigins, bofLines);
        trackFirstChanged(1);
    }
    const eofChangedLine = insertAtEnd(fileLines, lineOrigins, eofLines);
    if (eofChangedLine !== undefined)
        trackFirstChanged(eofChangedLine);
    return {
        text: fileLines.join("\n"),
        firstChangedLine,
        ...(warnings.length > 0 ? { warnings } : {}),
    };
}
//# sourceMappingURL=apply.js.map