feat(port): ported pi-mono improvements + worker & patch improvements
- added Azure OpenAI Responses support and OpenRouter routing compat - improved patch applicator diagnostics and subagent context propagation - updated editor cursor handling, keybindings, and working message API - refreshed porting sync metadata
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { resolveToCwd } from "../tools/path-utils";
|
||||
import { DEFAULT_FUZZY_THRESHOLD, findContextLine, findMatch, seekSequence } from "./fuzzy";
|
||||
import { DEFAULT_FUZZY_THRESHOLD, findClosestSequenceMatch, findContextLine, findMatch, seekSequence } from "./fuzzy";
|
||||
import {
|
||||
adjustIndentation,
|
||||
convertLeadingTabsToSpaces,
|
||||
@@ -67,9 +67,12 @@ interface Replacement {
|
||||
newLines: string[];
|
||||
}
|
||||
|
||||
type HunkVariantKind = "trim-common" | "dedupe-shared" | "collapse-repeated" | "single-line";
|
||||
|
||||
interface HunkVariant {
|
||||
oldLines: string[];
|
||||
newLines: string[];
|
||||
kind: HunkVariantKind;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -272,7 +275,7 @@ function trimCommonContext(oldLines: string[], newLines: string[]): HunkVariant
|
||||
if (trimmedOld.length === 0 && trimmedNew.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return { oldLines: trimmedOld, newLines: trimmedNew };
|
||||
return { oldLines: trimmedOld, newLines: trimmedNew, kind: "trim-common" };
|
||||
}
|
||||
|
||||
function collapseConsecutiveSharedLines(oldLines: string[], newLines: string[]): HunkVariant | undefined {
|
||||
@@ -297,7 +300,7 @@ function collapseConsecutiveSharedLines(oldLines: string[], newLines: string[]):
|
||||
if (collapsedOld.length === oldLines.length && collapsedNew.length === newLines.length) {
|
||||
return undefined;
|
||||
}
|
||||
return { oldLines: collapsedOld, newLines: collapsedNew };
|
||||
return { oldLines: collapsedOld, newLines: collapsedNew, kind: "dedupe-shared" };
|
||||
}
|
||||
|
||||
function collapseRepeatedBlocks(oldLines: string[], newLines: string[]): HunkVariant | undefined {
|
||||
@@ -339,7 +342,7 @@ function collapseRepeatedBlocks(oldLines: string[], newLines: string[]): HunkVar
|
||||
if (collapsedOld.length === oldLines.length && collapsedNew.length === newLines.length) {
|
||||
return undefined;
|
||||
}
|
||||
return { oldLines: collapsedOld, newLines: collapsedNew };
|
||||
return { oldLines: collapsedOld, newLines: collapsedNew, kind: "collapse-repeated" };
|
||||
}
|
||||
|
||||
function reduceToSingleLineChange(oldLines: string[], newLines: string[]): HunkVariant | undefined {
|
||||
@@ -352,12 +355,12 @@ function reduceToSingleLineChange(oldLines: string[], newLines: string[]): HunkV
|
||||
}
|
||||
}
|
||||
if (changedIndex === undefined) return undefined;
|
||||
return { oldLines: [oldLines[changedIndex]], newLines: [newLines[changedIndex]] };
|
||||
return { oldLines: [oldLines[changedIndex]], newLines: [newLines[changedIndex]], kind: "single-line" };
|
||||
}
|
||||
|
||||
function buildFallbackVariants(hunk: DiffHunk): HunkVariant[] {
|
||||
const variants: HunkVariant[] = [];
|
||||
const base: HunkVariant = { oldLines: hunk.oldLines, newLines: hunk.newLines };
|
||||
const base: HunkVariant = { oldLines: hunk.oldLines, newLines: hunk.newLines, kind: "trim-common" };
|
||||
|
||||
const trimmed = trimCommonContext(base.oldLines, base.newLines);
|
||||
if (trimmed) variants.push(trimmed);
|
||||
@@ -387,6 +390,11 @@ function buildFallbackVariants(hunk: DiffHunk): HunkVariant[] {
|
||||
});
|
||||
}
|
||||
|
||||
function filterFallbackVariants(variants: HunkVariant[], allowAggressive: boolean): HunkVariant[] {
|
||||
if (allowAggressive) return variants;
|
||||
return variants.filter(variant => variant.kind !== "collapse-repeated" && variant.kind !== "single-line");
|
||||
}
|
||||
|
||||
function findContextRelativeMatch(
|
||||
lines: string[],
|
||||
patternLine: string,
|
||||
@@ -414,6 +422,47 @@ function findContextRelativeMatch(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const AMBIGUITY_HINT_WINDOW = 200;
|
||||
const MATCH_PREVIEW_CONTEXT = 2;
|
||||
const MATCH_PREVIEW_MAX_LEN = 80;
|
||||
|
||||
function formatSequenceMatchPreview(lines: string[], startIdx: number): string {
|
||||
const start = Math.max(0, startIdx - MATCH_PREVIEW_CONTEXT);
|
||||
const end = Math.min(lines.length, startIdx + MATCH_PREVIEW_CONTEXT + 1);
|
||||
const previewLines = lines.slice(start, end);
|
||||
return previewLines
|
||||
.map((line, i) => {
|
||||
const num = start + i + 1;
|
||||
const truncated =
|
||||
line.length > MATCH_PREVIEW_MAX_LEN ? `${line.slice(0, MATCH_PREVIEW_MAX_LEN - 3)}...` : line;
|
||||
return ` ${num} | ${truncated}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function formatSequenceMatchPreviews(
|
||||
lines: string[],
|
||||
matchIndices: number[] | undefined,
|
||||
matchCount: number | undefined,
|
||||
): string | undefined {
|
||||
if (!matchIndices || matchIndices.length === 0) return undefined;
|
||||
const previews = matchIndices.map(index => formatSequenceMatchPreview(lines, index));
|
||||
const moreMsg =
|
||||
matchCount && matchCount > matchIndices.length ? ` (showing first ${matchIndices.length} of ${matchCount})` : "";
|
||||
return `${previews.join("\n\n")}${moreMsg}`;
|
||||
}
|
||||
|
||||
function chooseHintedMatch(
|
||||
matchIndices: number[] | undefined,
|
||||
hintIndex: number | undefined,
|
||||
window: number,
|
||||
): number | undefined {
|
||||
if (!matchIndices || matchIndices.length === 0 || hintIndex === undefined) return undefined;
|
||||
const candidates = matchIndices.filter(index => Math.abs(index - hintIndex) <= window);
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Get hint index from hunk's line number */
|
||||
function getHunkHintIndex(hunk: DiffHunk, currentIndex: number): number | undefined {
|
||||
if (hunk.oldStartLine === undefined) return undefined;
|
||||
@@ -458,11 +507,17 @@ function findHierarchicalContext(
|
||||
if (hintStart >= currentStart) {
|
||||
const hintedResult = findContextLine(lines, part, hintStart, { allowFuzzy });
|
||||
if (hintedResult.index !== undefined) {
|
||||
return { ...hintedResult, matchCount: 1 };
|
||||
return { ...hintedResult, matchCount: 1, matchIndices: [hintedResult.index] };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { index: undefined, confidence: result.confidence, matchCount: result.matchCount };
|
||||
return {
|
||||
index: undefined,
|
||||
confidence: result.confidence,
|
||||
matchCount: result.matchCount,
|
||||
matchIndices: result.matchIndices,
|
||||
strategy: result.strategy,
|
||||
};
|
||||
}
|
||||
|
||||
if (result.index === undefined) {
|
||||
@@ -471,7 +526,7 @@ function findHierarchicalContext(
|
||||
if (hintStart >= currentStart) {
|
||||
const hintedResult = findContextLine(lines, part, hintStart, { allowFuzzy });
|
||||
if (hintedResult.index !== undefined) {
|
||||
return { ...hintedResult, matchCount: 1 };
|
||||
return { ...hintedResult, matchCount: 1, matchIndices: [hintedResult.index] };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,17 +549,27 @@ function findHierarchicalContext(
|
||||
const inner = spaceParts[spaceParts.length - 1];
|
||||
const outerResult = findContextLine(lines, outer, startFrom, { allowFuzzy });
|
||||
if (outerResult.matchCount !== undefined && outerResult.matchCount > 1) {
|
||||
return { index: undefined, confidence: outerResult.confidence, matchCount: outerResult.matchCount };
|
||||
return {
|
||||
index: undefined,
|
||||
confidence: outerResult.confidence,
|
||||
matchCount: outerResult.matchCount,
|
||||
matchIndices: outerResult.matchIndices,
|
||||
strategy: outerResult.strategy,
|
||||
};
|
||||
}
|
||||
if (outerResult.index !== undefined) {
|
||||
const innerResult = findContextLine(lines, inner, outerResult.index + 1, { allowFuzzy });
|
||||
if (innerResult.index !== undefined) {
|
||||
return innerResult.matchCount && innerResult.matchCount > 1
|
||||
? { ...innerResult, matchCount: 1 }
|
||||
? { ...innerResult, matchCount: 1, matchIndices: [innerResult.index] }
|
||||
: innerResult;
|
||||
}
|
||||
if (innerResult.matchCount !== undefined && innerResult.matchCount > 1) {
|
||||
return { ...innerResult, matchCount: 1 };
|
||||
return {
|
||||
...innerResult,
|
||||
matchCount: 1,
|
||||
matchIndices: innerResult.index !== undefined ? [innerResult.index] : innerResult.matchIndices,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,7 +581,7 @@ function findHierarchicalContext(
|
||||
const hintStart = Math.max(0, lineHint - 1);
|
||||
const hintedResult = findContextLine(lines, context, hintStart, { allowFuzzy });
|
||||
if (hintedResult.index !== undefined) {
|
||||
return { ...hintedResult, matchCount: 1 };
|
||||
return { ...hintedResult, matchCount: 1, matchIndices: [hintedResult.index] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,7 +612,13 @@ function findHierarchicalContext(
|
||||
const outerResult = findContextLine(lines, outer, startFrom, { allowFuzzy });
|
||||
|
||||
if (outerResult.matchCount !== undefined && outerResult.matchCount > 1) {
|
||||
return { index: undefined, confidence: outerResult.confidence, matchCount: outerResult.matchCount };
|
||||
return {
|
||||
index: undefined,
|
||||
confidence: outerResult.confidence,
|
||||
matchCount: outerResult.matchCount,
|
||||
matchIndices: outerResult.matchIndices,
|
||||
strategy: outerResult.strategy,
|
||||
};
|
||||
}
|
||||
|
||||
if (outerResult.index === undefined) {
|
||||
@@ -556,10 +627,16 @@ function findHierarchicalContext(
|
||||
|
||||
const innerResult = findContextLine(lines, inner, outerResult.index + 1, { allowFuzzy });
|
||||
if (innerResult.index !== undefined) {
|
||||
return innerResult.matchCount && innerResult.matchCount > 1 ? { ...innerResult, matchCount: 1 } : innerResult;
|
||||
return innerResult.matchCount && innerResult.matchCount > 1
|
||||
? { ...innerResult, matchCount: 1, matchIndices: [innerResult.index] }
|
||||
: innerResult;
|
||||
}
|
||||
if (innerResult.matchCount !== undefined && innerResult.matchCount > 1) {
|
||||
return { ...innerResult, matchCount: 1 };
|
||||
return {
|
||||
...innerResult,
|
||||
matchCount: 1,
|
||||
matchIndices: innerResult.index !== undefined ? [innerResult.index] : innerResult.matchIndices,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,6 +697,7 @@ function attemptSequenceFallback(
|
||||
currentIndex: number,
|
||||
lineHint: number | undefined,
|
||||
allowFuzzy: boolean,
|
||||
allowAggressiveFallbacks: boolean,
|
||||
): number | undefined {
|
||||
if (hunk.oldLines.length === 0) return undefined;
|
||||
const matchHint = getHunkHintIndex(hunk, currentIndex);
|
||||
@@ -642,7 +720,7 @@ function attemptSequenceFallback(
|
||||
return fallbackResult.index;
|
||||
}
|
||||
|
||||
for (const variant of buildFallbackVariants(hunk)) {
|
||||
for (const variant of filterFallbackVariants(buildFallbackVariants(hunk), allowAggressiveFallbacks)) {
|
||||
if (variant.oldLines.length === 0) continue;
|
||||
const variantResult = findSequenceWithHint(
|
||||
lines,
|
||||
@@ -669,7 +747,7 @@ function applyCharacterMatch(
|
||||
hunk: DiffHunk,
|
||||
fuzzyThreshold: number,
|
||||
allowFuzzy: boolean,
|
||||
): string {
|
||||
): { content: string; warnings: string[] } {
|
||||
const oldText = hunk.oldLines.join("\n");
|
||||
const newText = hunk.newLines.join("\n");
|
||||
|
||||
@@ -725,10 +803,18 @@ function applyCharacterMatch(
|
||||
// Adjust indentation to match what was actually found
|
||||
const adjustedNewText = adjustIndentation(normalizedOldText, matchOutcome.match.actualText, newText);
|
||||
|
||||
const warnings: string[] = [];
|
||||
if (matchOutcome.dominantFuzzy && matchOutcome.match) {
|
||||
const similarity = Math.round(matchOutcome.match.confidence * 100);
|
||||
warnings.push(
|
||||
`Dominant fuzzy match selected in ${path} near line ${matchOutcome.match.startLine} (${similarity}% similar).`,
|
||||
);
|
||||
}
|
||||
|
||||
// Apply the replacement
|
||||
const before = normalizedContent.substring(0, matchOutcome.match.startIndex);
|
||||
const after = normalizedContent.substring(matchOutcome.match.startIndex + matchOutcome.match.actualText.length);
|
||||
return before + adjustedNewText + after;
|
||||
return { content: before + adjustedNewText + after, warnings };
|
||||
}
|
||||
|
||||
function applyTrailingNewlinePolicy(content: string, hadFinalNewline: boolean): string {
|
||||
@@ -746,8 +832,9 @@ function computeReplacements(
|
||||
path: string,
|
||||
hunks: DiffHunk[],
|
||||
allowFuzzy: boolean,
|
||||
): Replacement[] {
|
||||
): { replacements: Replacement[]; warnings: string[] } {
|
||||
const replacements: Replacement[] = [];
|
||||
const warnings: string[] = [];
|
||||
let lineIndex = 0;
|
||||
|
||||
for (const hunk of hunks) {
|
||||
@@ -763,6 +850,7 @@ function computeReplacements(
|
||||
);
|
||||
}
|
||||
const lineHint = hunk.oldStartLine;
|
||||
const allowAggressiveFallbacks = hunk.changeContext !== undefined || lineHint !== undefined || hunk.isEndOfFile;
|
||||
if (lineHint !== undefined && hunk.changeContext === undefined && !hunk.hasContextLines) {
|
||||
lineIndex = Math.max(0, Math.min(lineHint - 1, originalLines.length - 1));
|
||||
}
|
||||
@@ -775,16 +863,26 @@ function computeReplacements(
|
||||
contextIndex = idx;
|
||||
|
||||
if (idx === undefined || (result.matchCount !== undefined && result.matchCount > 1)) {
|
||||
const fallback = attemptSequenceFallback(originalLines, hunk, lineIndex, lineHint, allowFuzzy);
|
||||
const fallback = attemptSequenceFallback(
|
||||
originalLines,
|
||||
hunk,
|
||||
lineIndex,
|
||||
lineHint,
|
||||
allowFuzzy,
|
||||
allowAggressiveFallbacks,
|
||||
);
|
||||
if (fallback !== undefined) {
|
||||
lineIndex = fallback;
|
||||
} else if (result.matchCount !== undefined && result.matchCount > 1) {
|
||||
const displayContext = hunk.changeContext.includes("\n")
|
||||
? hunk.changeContext.split("\n").pop()
|
||||
: hunk.changeContext;
|
||||
const previews = formatSequenceMatchPreviews(originalLines, result.matchIndices, result.matchCount);
|
||||
const strategyHint = result.strategy ? ` Matching strategy: ${result.strategy}.` : "";
|
||||
const previewText = previews ? `\n\n${previews}` : "";
|
||||
throw new ApplyPatchError(
|
||||
`Found ${result.matchCount} matches for context '${displayContext}' in ${path}. ` +
|
||||
`Add more surrounding context or additional @@ anchors to make it unique.`,
|
||||
`Found ${result.matchCount} matches for context '${displayContext}' in ${path}.${strategyHint}` +
|
||||
`${previewText}\n\nAdd more surrounding context or additional @@ anchors to make it unique.`,
|
||||
);
|
||||
} else {
|
||||
const displayContext = hunk.changeContext.includes("\n")
|
||||
@@ -875,7 +973,7 @@ function computeReplacements(
|
||||
}
|
||||
|
||||
if (searchResult.index === undefined || (searchResult.matchCount ?? 0) > 1) {
|
||||
for (const variant of buildFallbackVariants(hunk)) {
|
||||
for (const variant of filterFallbackVariants(buildFallbackVariants(hunk), allowAggressiveFallbacks)) {
|
||||
if (variant.oldLines.length === 0) continue;
|
||||
const variantResult = findSequenceWithHint(
|
||||
originalLines,
|
||||
@@ -895,7 +993,7 @@ function computeReplacements(
|
||||
}
|
||||
|
||||
if (searchResult.index === undefined && contextIndex !== undefined) {
|
||||
for (const variant of buildFallbackVariants(hunk)) {
|
||||
for (const variant of filterFallbackVariants(buildFallbackVariants(hunk), allowAggressiveFallbacks)) {
|
||||
if (variant.oldLines.length !== 1 || variant.newLines.length !== 1) continue;
|
||||
const removedLine = variant.oldLines[0];
|
||||
const hasSharedDuplicate = hunk.newLines.some(line => line.trim() === removedLine.trim());
|
||||
@@ -929,11 +1027,38 @@ function computeReplacements(
|
||||
}
|
||||
}
|
||||
|
||||
if ((searchResult.matchCount ?? 0) > 1) {
|
||||
const hintIndex = matchHint ?? (lineHint ? lineHint - 1 : undefined);
|
||||
const hinted = chooseHintedMatch(searchResult.matchIndices, hintIndex, AMBIGUITY_HINT_WINDOW);
|
||||
if (hinted !== undefined) {
|
||||
searchResult = { ...searchResult, index: hinted, matchCount: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
if (searchResult.index === undefined) {
|
||||
if (searchResult.matchCount !== undefined && searchResult.matchCount > 1) {
|
||||
const previews = formatSequenceMatchPreviews(
|
||||
originalLines,
|
||||
searchResult.matchIndices,
|
||||
searchResult.matchCount,
|
||||
);
|
||||
const strategyHint = searchResult.strategy ? ` Matching strategy: ${searchResult.strategy}.` : "";
|
||||
const previewText = previews ? `\n\n${previews}` : "";
|
||||
throw new ApplyPatchError(
|
||||
`Found ${searchResult.matchCount} matches for the text in ${path}. ` +
|
||||
`Add more surrounding context or additional @@ anchors to make it unique.`,
|
||||
`Found ${searchResult.matchCount} matches for the text in ${path}.${strategyHint}` +
|
||||
`${previewText}\n\nAdd more surrounding context or additional @@ anchors to make it unique.`,
|
||||
);
|
||||
}
|
||||
const closest = findClosestSequenceMatch(originalLines, pattern, {
|
||||
start: lineIndex,
|
||||
eof: hunk.isEndOfFile,
|
||||
});
|
||||
if (closest.index !== undefined && closest.confidence > 0) {
|
||||
const similarity = Math.round(closest.confidence * 100);
|
||||
const preview = formatSequenceMatchPreview(originalLines, closest.index);
|
||||
throw new ApplyPatchError(
|
||||
`Failed to find expected lines in ${path}:\n${hunk.oldLines.join("\n")}\n\n` +
|
||||
`Closest match (${similarity}% similar) near line ${closest.index + 1}:\n${preview}`,
|
||||
);
|
||||
}
|
||||
throw new ApplyPatchError(`Failed to find expected lines in ${path}:\n${hunk.oldLines.join("\n")}`);
|
||||
@@ -941,11 +1066,23 @@ function computeReplacements(
|
||||
|
||||
const found = searchResult.index;
|
||||
|
||||
if (searchResult.strategy === "fuzzy-dominant") {
|
||||
const similarity = Math.round(searchResult.confidence * 100);
|
||||
warnings.push(`Dominant fuzzy match selected in ${path} near line ${found + 1} (${similarity}% similar).`);
|
||||
}
|
||||
|
||||
// Reject if match is ambiguous (prefix/substring matching found multiple matches)
|
||||
if (searchResult.matchCount !== undefined && searchResult.matchCount > 1) {
|
||||
const previews = formatSequenceMatchPreviews(
|
||||
originalLines,
|
||||
searchResult.matchIndices,
|
||||
searchResult.matchCount,
|
||||
);
|
||||
const strategyHint = searchResult.strategy ? ` Matching strategy: ${searchResult.strategy}.` : "";
|
||||
const previewText = previews ? `\n\n${previews}` : "";
|
||||
throw new ApplyPatchError(
|
||||
`Found ${searchResult.matchCount} matches for the text in ${path}. ` +
|
||||
`Add more surrounding context or additional @@ anchors to make it unique.`,
|
||||
`Found ${searchResult.matchCount} matches for the text in ${path}.${strategyHint}` +
|
||||
`${previewText}\n\nAdd more surrounding context or additional @@ anchors to make it unique.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -990,7 +1127,27 @@ function computeReplacements(
|
||||
// Sort by start index
|
||||
replacements.sort((a, b) => a.startIndex - b.startIndex);
|
||||
|
||||
return replacements;
|
||||
for (let i = 1; i < replacements.length; i++) {
|
||||
const prev = replacements[i - 1];
|
||||
const next = replacements[i];
|
||||
const prevEnd = prev.startIndex + prev.oldLen;
|
||||
if (next.startIndex < prevEnd) {
|
||||
const formatRange = (replacement: Replacement): string => {
|
||||
if (replacement.oldLen === 0) {
|
||||
return `${replacement.startIndex + 1} (insertion)`;
|
||||
}
|
||||
return `${replacement.startIndex + 1}-${replacement.startIndex + replacement.oldLen}`;
|
||||
};
|
||||
const prevRange = formatRange(prev);
|
||||
const nextRange = formatRange(next);
|
||||
throw new ApplyPatchError(
|
||||
`Overlapping hunks detected in ${path} at lines ${prevRange} and ${nextRange}. ` +
|
||||
`Split hunks or add more context to avoid overlap.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { replacements, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1018,7 +1175,7 @@ function applyHunksToContent(
|
||||
hunks: DiffHunk[],
|
||||
fuzzyThreshold: number,
|
||||
allowFuzzy: boolean,
|
||||
): string {
|
||||
): { content: string; warnings: string[] } {
|
||||
const hadFinalNewline = originalContent.endsWith("\n");
|
||||
|
||||
// Detect simple replace pattern: single hunk, no @@ context, no context lines, has old lines to match
|
||||
@@ -1032,8 +1189,8 @@ function applyHunksToContent(
|
||||
hunk.oldStartLine === undefined && // No line hint to use for positioning
|
||||
!hunk.isEndOfFile // No EOF targeting (prefer end of file)
|
||||
) {
|
||||
const content = applyCharacterMatch(originalContent, path, hunk, fuzzyThreshold, allowFuzzy);
|
||||
return applyTrailingNewlinePolicy(content, hadFinalNewline);
|
||||
const { content, warnings } = applyCharacterMatch(originalContent, path, hunk, fuzzyThreshold, allowFuzzy);
|
||||
return { content: applyTrailingNewlinePolicy(content, hadFinalNewline), warnings };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1048,7 +1205,7 @@ function applyHunksToContent(
|
||||
strippedTrailingEmpty = true;
|
||||
}
|
||||
|
||||
const replacements = computeReplacements(originalLines, path, hunks, allowFuzzy);
|
||||
const { replacements, warnings } = computeReplacements(originalLines, path, hunks, allowFuzzy);
|
||||
const newLines = applyReplacements(originalLines, replacements);
|
||||
|
||||
// Restore the trailing empty element if we stripped it
|
||||
@@ -1060,12 +1217,12 @@ function applyHunksToContent(
|
||||
|
||||
// Preserve original trailing newline behavior
|
||||
if (hadFinalNewline && !content.endsWith("\n")) {
|
||||
return `${content}\n`;
|
||||
return { content: `${content}\n`, warnings };
|
||||
}
|
||||
if (!hadFinalNewline && content.endsWith("\n")) {
|
||||
return content.slice(0, -1);
|
||||
return { content: content.slice(0, -1), warnings };
|
||||
}
|
||||
return content;
|
||||
return { content, warnings };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -1178,7 +1335,13 @@ async function applyNormalizedPatch(
|
||||
throw new ApplyPatchError("Diff contains no hunks");
|
||||
}
|
||||
|
||||
const newContent = applyHunksToContent(normalizedContent, input.path, hunks, fuzzyThreshold, allowFuzzy);
|
||||
const { content: newContent, warnings } = applyHunksToContent(
|
||||
normalizedContent,
|
||||
input.path,
|
||||
hunks,
|
||||
fuzzyThreshold,
|
||||
allowFuzzy,
|
||||
);
|
||||
const finalContent = bom + restoreLineEndings(newContent, lineEnding);
|
||||
const destPath = input.rename ? resolvePath(input.rename) : absolutePath;
|
||||
const isMove = Boolean(input.rename) && destPath !== absolutePath;
|
||||
@@ -1204,6 +1367,7 @@ async function applyNormalizedPatch(
|
||||
oldContent: originalContent,
|
||||
newContent: finalContent,
|
||||
},
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* fallback strategies for finding text in files.
|
||||
*/
|
||||
import { countLeadingWhitespace, normalizeForFuzzy, normalizeUnicode } from "./normalize";
|
||||
import type { ContextLineResult, FuzzyMatch, MatchOutcome, SequenceSearchResult } from "./types";
|
||||
import type { ContextLineResult, FuzzyMatch, MatchOutcome, SequenceMatchStrategy, SequenceSearchResult } from "./types";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Constants
|
||||
@@ -135,6 +135,7 @@ function computeLineOffsets(lines: string[]): number[] {
|
||||
interface BestFuzzyMatchResult {
|
||||
best?: FuzzyMatch;
|
||||
aboveThresholdCount: number;
|
||||
secondBestScore: number;
|
||||
}
|
||||
|
||||
function findBestFuzzyMatchCore(
|
||||
@@ -148,6 +149,7 @@ function findBestFuzzyMatchCore(
|
||||
|
||||
let best: FuzzyMatch | undefined;
|
||||
let bestScore = -1;
|
||||
let secondBestScore = -1;
|
||||
let aboveThresholdCount = 0;
|
||||
|
||||
for (let start = 0; start <= contentLines.length - targetLines.length; start++) {
|
||||
@@ -164,6 +166,7 @@ function findBestFuzzyMatchCore(
|
||||
}
|
||||
|
||||
if (score > bestScore) {
|
||||
secondBestScore = bestScore;
|
||||
bestScore = score;
|
||||
best = {
|
||||
actualText: windowLines.join("\n"),
|
||||
@@ -171,10 +174,12 @@ function findBestFuzzyMatchCore(
|
||||
startLine: start + 1,
|
||||
confidence: score,
|
||||
};
|
||||
} else if (score > secondBestScore) {
|
||||
secondBestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
return { best, aboveThresholdCount };
|
||||
return { best, aboveThresholdCount, secondBestScore };
|
||||
}
|
||||
|
||||
function findBestFuzzyMatch(content: string, target: string, threshold: number): BestFuzzyMatchResult {
|
||||
@@ -182,10 +187,10 @@ function findBestFuzzyMatch(content: string, target: string, threshold: number):
|
||||
const targetLines = target.split("\n");
|
||||
|
||||
if (targetLines.length === 0 || target.length === 0) {
|
||||
return { aboveThresholdCount: 0 };
|
||||
return { aboveThresholdCount: 0, secondBestScore: 0 };
|
||||
}
|
||||
if (targetLines.length > contentLines.length) {
|
||||
return { aboveThresholdCount: 0 };
|
||||
return { aboveThresholdCount: 0, secondBestScore: 0 };
|
||||
}
|
||||
|
||||
const offsets = computeLineOffsets(contentLines);
|
||||
@@ -257,14 +262,25 @@ export function findMatch(
|
||||
|
||||
// Try fuzzy match
|
||||
const threshold = options.threshold ?? DEFAULT_FUZZY_THRESHOLD;
|
||||
const { best, aboveThresholdCount } = findBestFuzzyMatch(content, target, threshold);
|
||||
const { best, aboveThresholdCount, secondBestScore } = findBestFuzzyMatch(content, target, threshold);
|
||||
|
||||
if (!best) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (options.allowFuzzy && best.confidence >= threshold && aboveThresholdCount === 1) {
|
||||
return { match: best, closest: best };
|
||||
if (options.allowFuzzy && best.confidence >= threshold) {
|
||||
if (aboveThresholdCount === 1) {
|
||||
return { match: best, closest: best };
|
||||
}
|
||||
const dominantDelta = 0.08;
|
||||
const dominantMin = 0.97;
|
||||
if (
|
||||
aboveThresholdCount > 1 &&
|
||||
best.confidence >= dominantMin &&
|
||||
best.confidence - secondBestScore >= dominantDelta
|
||||
) {
|
||||
return { match: best, closest: best, fuzzyMatches: aboveThresholdCount, dominantFuzzy: true };
|
||||
}
|
||||
}
|
||||
|
||||
return { closest: best, fuzzyMatches: aboveThresholdCount };
|
||||
@@ -360,7 +376,7 @@ export function seekSequence(
|
||||
const allowFuzzy = options?.allowFuzzy ?? true;
|
||||
// Empty pattern matches immediately
|
||||
if (pattern.length === 0) {
|
||||
return { index: start, confidence: 1.0 };
|
||||
return { index: start, confidence: 1.0, strategy: "exact" };
|
||||
}
|
||||
|
||||
// Pattern longer than available content cannot match
|
||||
@@ -376,35 +392,35 @@ export function seekSequence(
|
||||
// Pass 1: Exact match
|
||||
for (let i = from; i <= to; i++) {
|
||||
if (matchesAt(lines, pattern, i, (a, b) => a === b)) {
|
||||
return { index: i, confidence: 1.0 };
|
||||
return { index: i, confidence: 1.0, strategy: "exact" };
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: Trailing whitespace stripped
|
||||
for (let i = from; i <= to; i++) {
|
||||
if (matchesAt(lines, pattern, i, (a, b) => a.trimEnd() === b.trimEnd())) {
|
||||
return { index: i, confidence: 0.99 };
|
||||
return { index: i, confidence: 0.99, strategy: "trim-trailing" };
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3: Both leading and trailing whitespace stripped
|
||||
for (let i = from; i <= to; i++) {
|
||||
if (matchesAt(lines, pattern, i, (a, b) => a.trim() === b.trim())) {
|
||||
return { index: i, confidence: 0.98 };
|
||||
return { index: i, confidence: 0.98, strategy: "trim" };
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3b: Comment-prefix normalized match
|
||||
for (let i = from; i <= to; i++) {
|
||||
if (matchesAt(lines, pattern, i, (a, b) => stripCommentPrefix(a) === stripCommentPrefix(b))) {
|
||||
return { index: i, confidence: 0.975 };
|
||||
return { index: i, confidence: 0.975, strategy: "comment-prefix" };
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 4: Normalize unicode punctuation
|
||||
for (let i = from; i <= to; i++) {
|
||||
if (matchesAt(lines, pattern, i, (a, b) => normalizeUnicode(a) === normalizeUnicode(b))) {
|
||||
return { index: i, confidence: 0.97 };
|
||||
return { index: i, confidence: 0.97, strategy: "unicode" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,14 +432,16 @@ export function seekSequence(
|
||||
{
|
||||
let firstMatch: number | undefined;
|
||||
let matchCount = 0;
|
||||
const matchIndices: number[] = [];
|
||||
for (let i = from; i <= to; i++) {
|
||||
if (matchesAt(lines, pattern, i, lineStartsWithPattern)) {
|
||||
if (firstMatch === undefined) firstMatch = i;
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
}
|
||||
if (matchCount > 0) {
|
||||
return { index: firstMatch, confidence: 0.965, matchCount };
|
||||
return { index: firstMatch, confidence: 0.965, matchCount, matchIndices, strategy: "prefix" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,14 +449,16 @@ export function seekSequence(
|
||||
{
|
||||
let firstMatch: number | undefined;
|
||||
let matchCount = 0;
|
||||
const matchIndices: number[] = [];
|
||||
for (let i = from; i <= to; i++) {
|
||||
if (matchesAt(lines, pattern, i, lineIncludesPattern)) {
|
||||
if (firstMatch === undefined) firstMatch = i;
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
}
|
||||
if (matchCount > 0) {
|
||||
return { index: firstMatch, confidence: 0.94, matchCount };
|
||||
return { index: firstMatch, confidence: 0.94, matchCount, matchIndices, strategy: "substring" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,16 +484,22 @@ export function seekSequence(
|
||||
// Pass 7: Fuzzy matching - find best match above threshold
|
||||
let bestIndex: number | undefined;
|
||||
let bestScore = 0;
|
||||
let secondBestScore = 0;
|
||||
let matchCount = 0;
|
||||
const matchIndices: number[] = [];
|
||||
|
||||
for (let i = searchStart; i <= maxStart; i++) {
|
||||
const score = fuzzyScoreAt(lines, pattern, i);
|
||||
if (score >= SEQUENCE_FUZZY_THRESHOLD) {
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
if (score > bestScore) {
|
||||
secondBestScore = bestScore;
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
} else if (score > secondBestScore) {
|
||||
secondBestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,16 +509,31 @@ export function seekSequence(
|
||||
const score = fuzzyScoreAt(lines, pattern, i);
|
||||
if (score >= SEQUENCE_FUZZY_THRESHOLD) {
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
if (score > bestScore) {
|
||||
secondBestScore = bestScore;
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
} else if (score > secondBestScore) {
|
||||
secondBestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex !== undefined && bestScore >= SEQUENCE_FUZZY_THRESHOLD) {
|
||||
return { index: bestIndex, confidence: bestScore, matchCount };
|
||||
const dominantDelta = 0.08;
|
||||
const dominantMin = 0.97;
|
||||
if (matchCount > 1 && bestScore >= dominantMin && bestScore - secondBestScore >= dominantDelta) {
|
||||
return {
|
||||
index: bestIndex,
|
||||
confidence: bestScore,
|
||||
matchCount: 1,
|
||||
matchIndices,
|
||||
strategy: "fuzzy-dominant",
|
||||
};
|
||||
}
|
||||
return { index: bestIndex, confidence: bestScore, matchCount, matchIndices, strategy: "fuzzy" };
|
||||
}
|
||||
|
||||
// Pass 8: Character-based fuzzy matching via findMatch
|
||||
@@ -510,13 +551,59 @@ export function seekSequence(
|
||||
const matchedContent = contentText.substring(0, matchOutcome.match.startIndex);
|
||||
const lineIndex = start + matchedContent.split("\n").length - 1;
|
||||
const fallbackMatchCount = matchOutcome.occurrences ?? matchOutcome.fuzzyMatches ?? 1;
|
||||
return { index: lineIndex, confidence: matchOutcome.match.confidence, matchCount: fallbackMatchCount };
|
||||
return {
|
||||
index: lineIndex,
|
||||
confidence: matchOutcome.match.confidence,
|
||||
matchCount: fallbackMatchCount,
|
||||
strategy: "character",
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackMatchCount = matchOutcome.occurrences ?? matchOutcome.fuzzyMatches;
|
||||
return { index: undefined, confidence: bestScore, matchCount: fallbackMatchCount };
|
||||
}
|
||||
|
||||
export function findClosestSequenceMatch(
|
||||
lines: string[],
|
||||
pattern: string[],
|
||||
options?: { start?: number; eof?: boolean },
|
||||
): { index: number | undefined; confidence: number; strategy: SequenceMatchStrategy } {
|
||||
if (pattern.length === 0) {
|
||||
return { index: options?.start ?? 0, confidence: 1, strategy: "exact" };
|
||||
}
|
||||
if (pattern.length > lines.length) {
|
||||
return { index: undefined, confidence: 0, strategy: "fuzzy" };
|
||||
}
|
||||
|
||||
const start = options?.start ?? 0;
|
||||
const eof = options?.eof ?? false;
|
||||
const maxStart = lines.length - pattern.length;
|
||||
const searchStart = eof && lines.length >= pattern.length ? maxStart : start;
|
||||
|
||||
let bestIndex: number | undefined;
|
||||
let bestScore = 0;
|
||||
|
||||
for (let i = searchStart; i <= maxStart; i++) {
|
||||
const score = fuzzyScoreAt(lines, pattern, i);
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (eof && searchStart > start) {
|
||||
for (let i = start; i < searchStart; i++) {
|
||||
const score = fuzzyScoreAt(lines, pattern, i);
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { index: bestIndex, confidence: bestScore, strategy: "fuzzy" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a context line in the file using progressive matching strategies.
|
||||
*
|
||||
@@ -537,14 +624,16 @@ export function findContextLine(
|
||||
{
|
||||
let firstMatch: number | undefined;
|
||||
let matchCount = 0;
|
||||
const matchIndices: number[] = [];
|
||||
for (let i = startFrom; i < lines.length; i++) {
|
||||
if (lines[i] === context) {
|
||||
if (firstMatch === undefined) firstMatch = i;
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
}
|
||||
if (matchCount > 0) {
|
||||
return { index: firstMatch, confidence: 1.0, matchCount };
|
||||
return { index: firstMatch, confidence: 1.0, matchCount, matchIndices, strategy: "exact" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,14 +641,16 @@ export function findContextLine(
|
||||
{
|
||||
let firstMatch: number | undefined;
|
||||
let matchCount = 0;
|
||||
const matchIndices: number[] = [];
|
||||
for (let i = startFrom; i < lines.length; i++) {
|
||||
if (lines[i].trim() === trimmedContext) {
|
||||
if (firstMatch === undefined) firstMatch = i;
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
}
|
||||
if (matchCount > 0) {
|
||||
return { index: firstMatch, confidence: 0.99, matchCount };
|
||||
return { index: firstMatch, confidence: 0.99, matchCount, matchIndices, strategy: "trim" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,14 +659,16 @@ export function findContextLine(
|
||||
{
|
||||
let firstMatch: number | undefined;
|
||||
let matchCount = 0;
|
||||
const matchIndices: number[] = [];
|
||||
for (let i = startFrom; i < lines.length; i++) {
|
||||
if (normalizeUnicode(lines[i]) === normalizedContext) {
|
||||
if (firstMatch === undefined) firstMatch = i;
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
}
|
||||
if (matchCount > 0) {
|
||||
return { index: firstMatch, confidence: 0.98, matchCount };
|
||||
return { index: firstMatch, confidence: 0.98, matchCount, matchIndices, strategy: "unicode" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,15 +681,17 @@ export function findContextLine(
|
||||
if (contextNorm.length > 0) {
|
||||
let firstMatch: number | undefined;
|
||||
let matchCount = 0;
|
||||
const matchIndices: number[] = [];
|
||||
for (let i = startFrom; i < lines.length; i++) {
|
||||
const lineNorm = normalizeForFuzzy(lines[i]);
|
||||
if (lineNorm.startsWith(contextNorm)) {
|
||||
if (firstMatch === undefined) firstMatch = i;
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
}
|
||||
if (matchCount > 0) {
|
||||
return { index: firstMatch, confidence: 0.96, matchCount };
|
||||
return { index: firstMatch, confidence: 0.96, matchCount, matchIndices, strategy: "prefix" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,10 +708,17 @@ export function findContextLine(
|
||||
allSubstringMatches.push({ index: i, ratio });
|
||||
}
|
||||
}
|
||||
const matchIndices = allSubstringMatches.slice(0, 5).map(match => match.index);
|
||||
|
||||
// If exactly one substring match, accept it regardless of ratio
|
||||
if (allSubstringMatches.length === 1) {
|
||||
return { index: allSubstringMatches[0].index, confidence: 0.94, matchCount: 1 };
|
||||
return {
|
||||
index: allSubstringMatches[0].index,
|
||||
confidence: 0.94,
|
||||
matchCount: 1,
|
||||
matchIndices,
|
||||
strategy: "substring",
|
||||
};
|
||||
}
|
||||
|
||||
// Multiple matches: filter by ratio to disambiguate
|
||||
@@ -629,13 +731,19 @@ export function findContextLine(
|
||||
}
|
||||
}
|
||||
if (matchCount > 0) {
|
||||
return { index: firstMatch, confidence: 0.94, matchCount };
|
||||
return { index: firstMatch, confidence: 0.94, matchCount, matchIndices, strategy: "substring" };
|
||||
}
|
||||
|
||||
// If we had substring matches but none passed ratio filter,
|
||||
// return ambiguous result so caller knows matches exist
|
||||
if (allSubstringMatches.length > 1) {
|
||||
return { index: allSubstringMatches[0].index, confidence: 0.94, matchCount: allSubstringMatches.length };
|
||||
return {
|
||||
index: allSubstringMatches[0].index,
|
||||
confidence: 0.94,
|
||||
matchCount: allSubstringMatches.length,
|
||||
matchIndices,
|
||||
strategy: "substring",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,12 +751,14 @@ export function findContextLine(
|
||||
let bestIndex: number | undefined;
|
||||
let bestScore = 0;
|
||||
let matchCount = 0;
|
||||
const matchIndices: number[] = [];
|
||||
|
||||
for (let i = startFrom; i < lines.length; i++) {
|
||||
const lineNorm = normalizeForFuzzy(lines[i]);
|
||||
const score = similarity(lineNorm, contextNorm);
|
||||
if (score >= CONTEXT_FUZZY_THRESHOLD) {
|
||||
matchCount++;
|
||||
if (matchIndices.length < 5) matchIndices.push(i);
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
@@ -657,7 +767,7 @@ export function findContextLine(
|
||||
}
|
||||
|
||||
if (bestIndex !== undefined && bestScore >= CONTEXT_FUZZY_THRESHOLD) {
|
||||
return { index: bestIndex, confidence: bestScore, matchCount };
|
||||
return { index: bestIndex, confidence: bestScore, matchCount, matchIndices, strategy: "fuzzy" };
|
||||
}
|
||||
|
||||
if (!options?.skipFunctionFallback && trimmedContext.endsWith("()")) {
|
||||
|
||||
@@ -167,6 +167,27 @@ class LspFileSystem implements FileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
function mergeDiagnosticsWithWarnings(
|
||||
diagnostics: FileDiagnosticsResult | undefined,
|
||||
warnings: string[],
|
||||
): FileDiagnosticsResult | undefined {
|
||||
if (warnings.length === 0) return diagnostics;
|
||||
const warningMessages = warnings.map(warning => `patch: ${warning}`);
|
||||
if (!diagnostics) {
|
||||
return {
|
||||
server: "patch",
|
||||
messages: warningMessages,
|
||||
summary: `Patch warnings: ${warnings.length}`,
|
||||
errored: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...diagnostics,
|
||||
messages: [...warningMessages, ...diagnostics.messages],
|
||||
summary: `${diagnostics.summary}; Patch warnings: ${warnings.length}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Tool Class
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -319,9 +340,11 @@ export class EditTool implements AgentTool<TInput> {
|
||||
const flushedDiagnostics = await flushLspWritethroughBatch(batchRequest.id, this.session.cwd, signal);
|
||||
diagnostics ??= flushedDiagnostics;
|
||||
}
|
||||
const patchWarnings = result.warnings ?? [];
|
||||
const mergedDiagnostics = mergeDiagnosticsWithWarnings(diagnostics, patchWarnings);
|
||||
|
||||
const meta = outputMeta()
|
||||
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
||||
.diagnostics(mergedDiagnostics?.summary ?? "", mergedDiagnostics?.messages ?? [])
|
||||
.get();
|
||||
|
||||
return {
|
||||
@@ -329,7 +352,7 @@ export class EditTool implements AgentTool<TInput> {
|
||||
details: {
|
||||
diff: diffResult.diff,
|
||||
firstChangedLine: diffResult.firstChangedLine,
|
||||
diagnostics,
|
||||
diagnostics: mergedDiagnostics,
|
||||
op,
|
||||
rename: effRename,
|
||||
meta,
|
||||
|
||||
@@ -328,6 +328,11 @@ function parseOneHunk(lines: string[], lineNumber: number, allowMissingContext:
|
||||
for (let i = startIndex; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
const nextLine = lines[i + 1];
|
||||
|
||||
if (line === "" && parsedLines > 0 && nextLine?.trimStart().startsWith("@@")) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isDiffContentLine(line) && line.trimEnd() === EOF_MARKER && line.startsWith(EOF_MARKER)) {
|
||||
if (parsedLines === 0) {
|
||||
|
||||
@@ -46,9 +46,23 @@ export interface MatchOutcome {
|
||||
occurrencePreviews?: string[];
|
||||
/** Number of fuzzy matches above threshold */
|
||||
fuzzyMatches?: number;
|
||||
/** True when a dominant fuzzy match was accepted despite multiple candidates */
|
||||
dominantFuzzy?: boolean;
|
||||
}
|
||||
|
||||
/** Result of a sequence search */
|
||||
export type SequenceMatchStrategy =
|
||||
| "exact"
|
||||
| "trim-trailing"
|
||||
| "trim"
|
||||
| "comment-prefix"
|
||||
| "unicode"
|
||||
| "prefix"
|
||||
| "substring"
|
||||
| "fuzzy"
|
||||
| "fuzzy-dominant"
|
||||
| "character";
|
||||
|
||||
export interface SequenceSearchResult {
|
||||
/** Starting line index of the match (0-indexed) */
|
||||
index: number | undefined;
|
||||
@@ -56,9 +70,15 @@ export interface SequenceSearchResult {
|
||||
confidence: number;
|
||||
/** Number of matches at the same confidence level (for ambiguity detection) */
|
||||
matchCount?: number;
|
||||
/** Sample of matching indices (0-indexed, up to a small limit) */
|
||||
matchIndices?: number[];
|
||||
/** Matching strategy used */
|
||||
strategy?: SequenceMatchStrategy;
|
||||
}
|
||||
|
||||
/** Result of a context line search */
|
||||
export type ContextMatchStrategy = "exact" | "trim" | "unicode" | "prefix" | "substring" | "fuzzy";
|
||||
|
||||
export interface ContextLineResult {
|
||||
/** Index of the matching line (0-indexed) */
|
||||
index: number | undefined;
|
||||
@@ -66,6 +86,10 @@ export interface ContextLineResult {
|
||||
confidence: number;
|
||||
/** Number of matches at the same confidence level (for ambiguity detection) */
|
||||
matchCount?: number;
|
||||
/** Sample of matching indices (0-indexed, up to a small limit) */
|
||||
matchIndices?: number[];
|
||||
/** Matching strategy used */
|
||||
strategy?: ContextMatchStrategy;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -133,6 +157,7 @@ export interface FileChange {
|
||||
/** Result of applying a patch */
|
||||
export interface ApplyPatchResult {
|
||||
change: FileChange;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
/** Options for applying a patch */
|
||||
|
||||
Reference in New Issue
Block a user