/** * Core smart editing logic — whitespace-tolerant text replacement. * * Designed to prevent local/quantized LLMs from failing at str_replace edits * due to indentation mismatches, trailing whitespace, or Unicode character drift. * * Strategy (progressive fallback, fail-closed on structure): * 1. Exact match — preserves original bytes (fastest) * 2. Fuzzy match — strip trailing whitespace, normalize Unicode (line-level replace) * 3. Relative-indent match — same content after fuzzy+strip, with matching * relative indentation shape between lines (absolute indent may differ) * * Aggressive pure strip-without-structure matching is intentionally rejected: * it can match the wrong block and corrupt indent-sensitive languages (Python). * * For non-exact matches, we operate at the LINE level: find which original * lines match after normalization, replace those lines, preserve the rest. * This avoids byte-offset corruption when the matched region differs in * indentation from the model's oldText. * * EDITOMATIC_STRICT=1 — only allow exact and fuzzy strategies (emergency lockdown). */ /** * Strip UTF-8 BOM if present. */ export declare function stripBom(content: string): { bom: string; text: string; }; /** * Normalize all line endings to LF. */ export declare function normalizeToLF(text: string): string; /** * Restore the original line ending style. */ export declare function detectLineEnding(content: string): '\n' | '\r\n'; export declare function restoreLineEndings(text: string, ending: '\n' | '\r\n'): string; /** * Basic fuzzy normalization: strip trailing whitespace per line, normalize * Unicode smart quotes, dashes, special spaces, box-drawing chars, emoji, * and full-width ASCII to ASCII equivalents. * Leading whitespace (indentation) is PRESERVED. */ export declare function normalizeForFuzzyMatch(text: string): string; /** * Strip ALL leading whitespace (spaces and tabs) from every line. * This neutralizes indentation entirely — useful when the model gets * indentation levels wrong. */ export declare function stripLeadingWhitespace(text: string): string; /** * Combined normalization: fuzzy + indentation-stripped. */ export declare function normalizeFully(text: string): string; export interface IndentInfo { style: 'tabs' | 'spaces' | 'mixed'; /** * Indent **unit** (not the most common absolute indent depth). * For spaces: GCD-based step size (e.g. 4 for a file with levels 4/8/12). * For tabs: 1 (one tab per level). */ width: number; /** * How many visual columns a single tab represents. Used for visual-width-based * indent calculations when the original uses tabs. Default: 4. */ tabWidth?: number; /** Distinct absolute space-indent lengths observed, sorted ascending. */ observedLevels?: number[]; } /** Greatest common divisor for positive integers. */ export declare function gcd(a: number, b: number): number; /** * Infer space indent unit from absolute leading-space lengths. * Prefers GCD of levels/deltas so nested bodies (8 cols) do not report "8-space" * when the file uses 4-space steps. */ export declare function detectSpaceIndentUnit(lengths: number[]): { width: number; observedLevels: number[]; }; /** * Human-readable indent summary for tool messages and prompt injection. * Example: "spaces, 4-space indent unit (levels seen: 4, 8, 12)" */ export declare function formatIndentHint(info: IndentInfo | null): string; /** * Calculate the visual width of an indent string, expanding tabs to tabWidth spaces. */ export declare function visualIndentWidth(indent: string, tabWidth?: number): number; /** * Minimum visual indent among non-blank lines. Blank lines are ignored. * Returns 0 when there are no non-blank lines. */ export declare function minNonBlankVisualIndent(lines: string[], tabWidth?: number): number; /** * Relative-indent fingerprint for a block of lines. * * Absolute indent is removed by subtracting the block's min non-blank indent. * Content is fuzzy-normalized (trailing ws + Unicode). Blank lines → "B". * * Two blocks with the same relative structure and same content fingerprint * equal even when their absolute indent levels differ. Blocks that only match * after destroying relative structure will not. */ export declare function relativeIndentFingerprint(lines: string[], tabWidth?: number): string[]; /** * Drop trailing blank (whitespace-only) lines. Used for match retries when the * model includes an extra trailing newline in oldText. */ export declare function trimTrailingBlankLines(lines: string[]): string[]; /** * Find all line windows whose relative-indent fingerprint equals oldLines. * This is the safe multi-line fallback: tolerates absolute indent drift, * rejects structure-destroying matches. */ export declare function findRelativeIndentMatches(contentLines: string[], oldLines: string[], tabWidth?: number): Array<{ start: number; count: number; }>; /** True when EDITOMATIC_STRICT is set to a truthy value (1/true/yes). */ export declare function isStrictMode(): boolean; /** * Detect indentation style and **unit** in a file. * Returns info for feedback / reindent; matching does not depend on this. * * For spaces, `width` is the indent step (GCD of observed levels), not the most * common absolute depth — so a 4-space Python file full of 8-col method bodies * reports 4, not 8. */ export declare function detectIndentation(content: string): IndentInfo | null; /** * Information about a file's indentation, suitable for model prompt injection. */ export interface PromptIndentInfo { /** File path. */ path: string; /** Detected indentation style. 'none' if file has no indented lines. */ style: 'tabs' | 'spaces' | 'mixed' | 'none'; /** Indent unit (GCD-based for spaces, 1 for tabs). */ width: number; /** How many visual columns a tab represents. Default: 4. */ tabWidth: number; /** Base indent level in visual columns for the first indented line. */ baseIndent: number; /** Observed absolute space-indent lengths, if any. */ observedLevels?: number[]; } /** * Build prompt-indent-info from file content. Returns info the model can use * to generate correctly-indented newText. */ export declare function buildPromptIndentInfo(content: string, path: string): PromptIndentInfo; /** * One prompt-ready block describing a file's indentation for model context. * Includes short line examples and a Python-specific syntax reminder. * Returns null when the file has no usable indent info. */ export declare function formatFileIndentContext(content: string, path: string): string | null; export interface MatchResult { found: boolean; /** * Strategy used for matching. * - 'exact': byte-perfect match, original indices valid * - 'fuzzy': trailing whitespace/unicode normalization; LINE-LEVEL match * - 'indentation': relative-indent fingerprint match (absolute indent may * differ; relative structure must match). Alias retained for API stability; * pure strip-without-structure matching is no longer used. * - 'line-by-line': retained for API stability; no longer produced * - 'none': not found */ strategy: 'exact' | 'fuzzy' | 'indentation' | 'line-by-line' | 'none'; /** * The original lines (0-based) that matched, for non-exact strategies. * Required for safe line-level replacement. */ matchedLineRange?: { start: number; count: number; }; details?: string; /** If multiple matches were found, the number of occurrences. */ matchCount?: number; /** If true, the oldText matched multiple locations. */ ambiguous?: boolean; /** Byte offset of the first match (only set for exact strategy). */ exactByteIndex?: number; } /** * Find oldText in content with progressive fallback strategies. * Also detects multiple-match ambiguity. * * Fail-closed: aggressive strategies only accept matches whose relative * indentation fingerprint agrees with oldText (structure preserved). * * After exact/fuzzy/relative fail, retries with trailing blank lines stripped * from oldText (common when models include an extra final newline on large blocks). */ export declare function smartFindText(content: string, oldText: string): MatchResult; /** * Build a human-readable explanation when oldText cannot be found. * Helps models fix the next call without guessing (first differing line, nearest * region, trailing-newline / whitespace notes). Caps output size for context. */ export declare function diagnoseMatchFailure(content: string, oldText: string, path: string, tabWidth?: number): string; export interface EditOp { oldText: string; newText: string; } /** Final line range (0-based start, line count) of an applied edit in new content. */ export interface AppliedEditRange { editIndex: number; startLine: number; lineCount: number; } /** * Adjust the indentation of replacement text to match the base indent * level of the original lines being replaced. * * Base indent is the **minimum** visual indent of non-blank lines (not the * first non-zero indent line). This preserves multi-level structure when the * matched region includes column-0 headers (e.g. class + methods) or when * the model provides de-indented but relatively correct newText. * * Uses visual-width-based indent calculations and preserves the original * file's tab/space convention. */ export declare function reindentReplacement(originalLines: string[], newText: string, indentInfo: IndentInfo | null, tabWidth?: number): string; /** * Replace specific lines in content with newText. * - Empty newText: delete matched lines * - "\n" newText: insert a blank line * - Preserve intentional trailing newlines when original doesn't end with one */ export declare function replaceLines(content: string, startLine: number, count: number, newText: string): string; /** * Count lines the same way String#split('\n') does (trailing newline yields * an extra empty segment). Used to measure real line deltas after apply. */ export declare function countLines(text: string): number; /** * Apply one or more edits to content, returning the new content and any * matching details. * * Non-exact strategies always use line-level replacement. The previous * fuzzy byte-offset fallback (normalized offsets on original content) is * intentionally removed — it could corrupt adjacent bytes. * * Multi-edit application order: descending original position, so earlier * regions are not displaced by later replacements. lineOffset is measured * from actual content before/after each edit (not inferred from split length * of replacement strings alone). */ export declare function applyEdits(content: string, edits: EditOp[], path: string, indentInfo?: IndentInfo | null, tabWidth?: number): { newContent: string; details: string[]; appliedRanges: AppliedEditRange[]; }; export type DiffEdit = { type: 'equal'; oldIndex: number; newIndex: number; text: string; } | { type: 'delete'; oldIndex: number; text: string; } | { type: 'insert'; newIndex: number; text: string; }; /** * Myers O(ND) shortest-edit script for two line arrays. * Returns a sequence of equal / delete / insert ops covering both sequences. * * @see Eugene W. Myers, "An O(ND) Difference Algorithm and Its Variations" * @see https://blog.jcoglan.com/2017/02/15/the-myers-diff-algorithm-part-2/ */ export declare function myersLineDiff(a: string[], b: string[]): DiffEdit[]; /** * Format Myers line edits into the display string used by Pi's renderDiff. * * Format (compatible with previous generateSimpleDiff output): * - Context: ` ${lineNum} ${text}` * - Delete: `-${lineNum} ${text}` * - Insert: `+${lineNum} ${text}` * * Unchanged regions outside `contextLines` of a change are omitted; a * single ` ...` separator is inserted between distant hunks. */ export declare function formatDiffEdits(edits: DiffEdit[], contextLines?: number): string; /** * Generate a line-oriented unified-style diff for display. * * Uses Myers' O(ND) algorithm so insertions/deletions with line-count changes * no longer produce interleaved garbage (the old index-aligned comparison). * * Kept name `generateSimpleDiff` for API stability. */ export declare function generateSimpleDiff(oldContent: string, newContent: string, contextLines?: number): string; /** Alias for clarity; same as generateSimpleDiff. */ export declare const generateMyersDiff: typeof generateSimpleDiff; //# sourceMappingURL=smart-edit.d.ts.map