//#region src/tools/edit-utils.ts
/**
* Internal helpers shared between the `edit` and `multi_edit` tools.
*
* Not part of the public API — intentionally not re-exported from `tools/index.ts`
* or the package barrel.
*/
/**
* Count exact (non-overlapping) occurrences of `needle` in `haystack`.
* Returns 0 for an empty needle — both edit tools reject empty `old_string`
* up front, so this branch is defensive rather than semantic.
*/
function countExactMatches(haystack, needle) {
	if (needle.length === 0) return 0;
	let count = 0;
	let idx = 0;
	while (true) {
		const next = haystack.indexOf(needle, idx);
		if (next === -1) break;
		count++;
		idx = next + needle.length;
	}
	return count;
}
/** Map curly quotes (any of the four) to their straight ASCII equivalents. */
function normalizeQuotes(str) {
	return str.replaceAll("‘", "'").replaceAll("’", "'").replaceAll("“", "\"").replaceAll("”", "\"");
}
/**
* Substitutions Anthropic's API applies to assistant output before the model
* sees it. The model emits the sanitized form; the file on disk contains the
* unsanitized form. We undo the substitutions on `old_string` so the search
* lands on the actual file contents.
*
* Verbatim from `claude-code/tools/FileEditTool/utils.ts`.
*/
const DESANITIZATIONS = [
	["<fnr>", "<function_results>"],
	["<n>", "<name>"],
	["</n>", "</name>"],
	["<o>", "<output>"],
	["</o>", "</output>"],
	["<e>", "<error>"],
	["</e>", "</error>"],
	["<s>", "<system>"],
	["</s>", "</system>"],
	["<r>", "<result>"],
	["</r>", "</result>"],
	["< META_START >", "<META_START>"],
	["< META_END >", "<META_END>"],
	["< EOT >", "<EOT>"],
	["< META >", "<META>"],
	["< SOS >", "<SOS>"],
	["\n\nH:", "\n\nHuman:"],
	["\n\nA:", "\n\nAssistant:"]
];
/**
* Apply the SDK desanitization table to a string. Exported so the edit tools
* can apply it to `new_string` whenever `old_string` matched via a
* desanitize-class fallback — keeps the file's unsanitized form on disk
* instead of writing the model's abbreviated form back.
*/
function desanitize(s) {
	let out = s;
	for (const [from, to] of DESANITIZATIONS) out = out.replaceAll(from, to);
	return out;
}
/**
* Strip line-number prefixes from each line of a needle, used as a recovery
* fallback when the model pastes a `read_file` chunk verbatim into
* `old_string` — the on-disk file doesn't carry the metadata prefix.
*
* Accepts three separator characters so a model that learned on a different
* agent stack still works here: `\t` (Claude Code compact, our default),
* `|`, and `→`. Pattern: optional leading whitespace, 1-9 digits, then one
* of `\t | →`. The 9-digit ceiling covers files up to ~1B lines without
* overshooting into legitimate `\d{N}<sep>` content like Markdown table
* cells with long numeric IDs.
*/
const LINE_NUMBER_PREFIX_RE = /^[ \t]*\d{1,9}[\t|\u2192]/gm;
function stripLineNumberPrefixes(s) {
	return s.replace(LINE_NUMBER_PREFIX_RE, "");
}
/**
* Search `target` in `normFile` and slice the matching span out of the
* original `haystack`, counting all non-overlapping occurrences. `normFile`
* is the haystack with whatever transform (quotes / desanitize / combined)
* was applied to make the indices align — slicing the original haystack
* preserves the file's actual typography so `replace_all` writes back the
* file's form, not the model's.
*
* Pre-condition: `normFile.length === haystack.length` (every transform
* we use is one-to-one). Returns null on miss.
*/
function locateAndCount(haystack, normFile, target, via) {
	const idx = normFile.indexOf(target);
	if (idx === -1) return null;
	const actual = haystack.slice(idx, idx + target.length);
	let occ = 0;
	let cursor = 0;
	while (true) {
		const next = normFile.indexOf(target, cursor);
		if (next === -1) break;
		occ++;
		cursor = next + target.length;
	}
	return {
		actual,
		occurrences: occ,
		via
	};
}
function resolveOldString(haystack, needle) {
	const exact = countExactMatches(haystack, needle);
	if (exact > 0) return {
		actual: needle,
		occurrences: exact,
		via: "exact"
	};
	const normNeedle = normalizeQuotes(needle);
	const normFile = normalizeQuotes(haystack);
	if (normNeedle !== needle || normFile !== haystack) {
		const m = locateAndCount(haystack, normFile, normNeedle, "quotes");
		if (m) return m;
	}
	const desan = desanitize(needle);
	if (desan !== needle) {
		const desanCount = countExactMatches(haystack, desan);
		if (desanCount === 1) return {
			actual: desan,
			occurrences: desanCount,
			via: "desanitize"
		};
	}
	const combo = desanitize(normNeedle);
	if (combo !== needle) {
		const m = locateAndCount(haystack, normFile, combo, "quotes+desanitize");
		if (m && m.occurrences === 1) return m;
	}
	const stripped = stripLineNumberPrefixes(needle);
	if (stripped !== needle && stripped.trim().length > 0) {
		const count = countExactMatches(haystack, stripped);
		if (count > 0) return {
			actual: stripped,
			occurrences: count,
			via: "line-numbers"
		};
		const strippedNorm = normalizeQuotes(stripped);
		if (strippedNorm !== stripped || normFile !== haystack) {
			const m = locateAndCount(haystack, normFile, strippedNorm, "quotes+line-numbers");
			if (m) return m;
		}
	}
	return null;
}
/**
* Short human-readable label for a recovery path, used by `edit` /
* `multi_edit` to flag non-exact matches in their result messages — when a
* fallback mistargets, the model (and the user reading the transcript) can
* see WHICH transform fired instead of silently trusting the edit.
*/
function describeVia(via) {
	switch (via) {
		case "exact": return "exact match";
		case "quotes": return "curly-quote normalization";
		case "desanitize": return "sanitization recovery (e.g. <n> → <name>)";
		case "quotes+desanitize": return "sanitization recovery + curly-quote normalization";
		case "line-numbers": return "line-number-prefix stripping";
		case "quotes+line-numbers": return "line-number-prefix stripping + curly-quote normalization";
	}
}
/**
* Apply the same recovery transforms used to find `old_string` to
* `new_string`, so the file gets back its native form: desanitize when
* the model emitted `<n>` for `<name>`, strip line-number prefixes when
* the match required them, then re-curlify when the match required
* quote normalization. Shared between `edit` and `multi_edit`.
*/
function styleReplacementForVia(replacement, via, actual) {
	let out = replacement;
	if (via === "desanitize" || via === "quotes+desanitize") out = desanitize(out);
	if (via === "line-numbers" || via === "quotes+line-numbers") out = stripLineNumberPrefixes(out);
	if (via === "quotes" || via === "quotes+desanitize" || via === "quotes+line-numbers") out = preserveQuoteStyle(actual, out);
	return out;
}
/**
* When `old_string` matched via curly-quote normalization, re-style
* `new_string` so the file's typography is preserved across the edit.
* Detects whether the matched file region had curly singles, doubles, or
* both, and applies the matching curlification to the replacement.
*
* Apostrophes in contractions (`don't`, `it's`) get the right-single curly
* quote regardless of opening context — that's the canonical typographer's
* convention for English. Other quotes use a simple
* preceded-by-whitespace-or-opening-punctuation heuristic.
*/
function preserveQuoteStyle(actual, replacement) {
	const hasDouble = actual.includes("“") || actual.includes("”");
	const hasSingle = actual.includes("‘") || actual.includes("’");
	if (!hasDouble && !hasSingle) return replacement;
	let out = replacement;
	if (hasDouble) out = applyCurly(out, "\"", "“", "”", false);
	if (hasSingle) out = applyCurly(out, "'", "‘", "’", true);
	return out;
}
function applyCurly(s, straight, left, right, contractionAware) {
	const chars = [...s];
	const result = [];
	for (let i = 0; i < chars.length; i++) {
		if (chars[i] !== straight) {
			result.push(chars[i]);
			continue;
		}
		if (contractionAware) {
			const prev = i > 0 ? chars[i - 1] : "";
			const next = i < chars.length - 1 ? chars[i + 1] : "";
			if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
				result.push(right);
				continue;
			}
		}
		result.push(isOpeningContext(chars, i) ? left : right);
	}
	return result.join("");
}
function isOpeningContext(chars, i) {
	if (i === 0) return true;
	const prev = chars[i - 1];
	return prev === " " || prev === "	" || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "—" || prev === "–";
}
//#endregion
export { styleReplacementForVia as i, resolveOldString as n, stripLineNumberPrefixes as r, describeVia as t };

//# sourceMappingURL=edit-utils-EGosADZq.js.map