"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.XtabCustomDiffPatchResponseHandler = exports.DuplicateAdditionsMode = void 0;
exports.tryRemoveDuplicateAdditions = tryRemoveDuplicateAdditions;
const documentId_1 = require("../../../platform/inlineEdits/common/dataTypes/documentId");
const xtabPromptOptions_1 = require("../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions");
Object.defineProperty(exports, "DuplicateAdditionsMode", { enumerable: true, get: function () { return xtabPromptOptions_1.DuplicateAdditionsMode; } });
const statelessNextEditProvider_1 = require("../../../platform/inlineEdits/common/statelessNextEditProvider");
const errors_1 = require("../../../util/common/errors");
const arrays_1 = require("../../../util/vs/base/common/arrays");
const path_1 = require("../../../util/vs/base/common/path");
const uri_1 = require("../../../util/vs/base/common/uri");
const lineEdit_1 = require("../../../util/vs/editor/common/core/edits/lineEdit");
const lineRange_1 = require("../../../util/vs/editor/common/core/ranges/lineRange");
const fetchStreamError_1 = require("../common/fetchStreamError");
const promptCraftingUtils_1 = require("../common/promptCraftingUtils");
const tags_1 = require("../common/tags");
class Patch {
    constructor(
    /**
     * Expected to be file path relative to workspace root.
     */
    filePath, lineNumZeroBased) {
        this.filePath = filePath;
        this.lineNumZeroBased = lineNumZeroBased;
        this.removedLines = [];
        this.addedLines = [];
    }
    static ofLine(line) {
        const match = line.match(/^(.+):(\d+)$/);
        if (!match) {
            return null;
        }
        const [, filename, lineNumber] = match;
        return new Patch(filename, parseInt(lineNumber, 10));
    }
    addLine(line) {
        const contentLine = line.slice(1);
        if (line.startsWith('-')) {
            this.removedLines.push(contentLine);
            return true;
        }
        else if (line.startsWith('+')) {
            this.addedLines.push(contentLine);
            return true;
        }
        else {
            return false;
        }
    }
    toString() {
        return [
            `${this.filePath}:${this.lineNumZeroBased}`,
            ...this.removedLines.map(l => `-${l}`),
            ...this.addedLines.map(l => `+${l}`),
        ].join('\n');
    }
}
/**
 * A heuristic line is "meaningful" enough to base a duplicate detection on
 * if it has more than one non-whitespace character. This guards against
 * matching common single-token lines (`}`, `;`, `)`) and blank lines, which
 * frequently appear both in legitimate additions and in surrounding context.
 */
function isMeaningfulLine(line) {
    return line.trim().length > 1;
}
/**
 * Checks whether the patch's added lines duplicate the file content immediately
 * following the deleted range, and if so, trims the duplicated lines.
 *
 * The diff-patch model can occasionally regress and emit additions that copy
 * the existing continuation code. When that happens, applying the patch
 * produces visibly duplicated lines in the file (e.g. an extra closing brace
 * after a code block).
 *
 * Three duplication shapes are detected, mirroring the heuristic used in the
 * offline analysis tooling. Only one shape is applied per patch — the checks
 * run in priority order and the first match wins. This avoids cascading
 * removals where one shape's trimming exposes a spurious match for another.
 *
 * Priority order:
 *   1. **Suffix** — last `k` additions match first `k` following lines (most
 *      common case: trailing `}` re-emitted). Picks the longest matching `k`.
 *      No additional ambiguity guard is applied: the prior heuristic
 *      preserved single-token suffixes when they consumed the entire
 *      addition (e.g. `addedLines = ['}']` with `following = ['}']`), but
 *      that's the headline-bug case — applying the patch would insert a
 *      stray closing token into already-balanced code. The trade-off is a
 *      rare false positive when the file is mid-typing and unbalanced; the
 *      common case (file is balanced, model is duplicating) is the one we
 *      optimize for.
 *   2. **Prefix** — first addition matches first following line, and the line
 *      is "meaningful" (>1 non-whitespace char) to avoid dropping legitimate
 *      single-token lines (e.g. an inner-scope `}` followed by an outer `}`).
 *   3. **Middle** — a consecutive pair of additions starting at offset > 0
 *      (and not at the end) matches the first two following lines. Both
 *      following lines must be meaningful, again to avoid trivial matches.
 *      The match is then **greedily extended**: only the verified-equal
 *      lines are dropped; any addition lines beyond the equal range are
 *      preserved (the model may have appended new content after the
 *      streaming-style regenerated context).
 *
 * Returns a `DuplicateAdditionRemoval` describing what was dropped, or
 * `undefined` if no duplication was detected.
 */
function tryRemoveDuplicateAdditions(replacement, currentText) {
    const addedLines = replacement.newLines;
    if (addedLines.length === 0) {
        return undefined;
    }
    // All line numbers here are 1-based (matching `LineRange` and
    // `AbstractText.getLineAt`). `endLineNumberExclusive` is the first line
    // after the deletion — i.e. the first "following" line.
    const followingStartLine1Based = replacement.lineRange.endLineNumberExclusive;
    const fileLineCount = currentText.lineRange.endLineNumberExclusive - 1;
    const followingEndLine1BasedExclusive = Math.min(fileLineCount + 1, followingStartLine1Based + addedLines.length);
    const following = [];
    for (let l = followingStartLine1Based; l < followingEndLine1BasedExclusive; l++) {
        following.push(currentText.getLineAt(l));
    }
    if (following.length === 0) {
        return undefined;
    }
    // 1. Suffix: longest k where last k additions match first k following lines.
    for (let k = Math.min(addedLines.length, following.length); k >= 1; k--) {
        const tail = addedLines.slice(-k);
        if (!(0, arrays_1.equals)(tail, following.slice(0, k))) {
            continue;
        }
        return {
            kind: 'suffix',
            newAdditions: addedLines.slice(0, addedLines.length - k),
            removedLines: tail,
        };
    }
    // 2. Prefix: first addition matches first following line and is meaningful.
    if (addedLines[0] === following[0] && isMeaningfulLine(addedLines[0])) {
        return {
            kind: 'prefix',
            newAdditions: addedLines.slice(1),
            removedLines: addedLines.slice(0, 1),
        };
    }
    // 3. Middle (e.g. streamEdits): consecutive pair of additions at offset > 0
    //    (and not extending to the end — that case is suffix) matches the first
    //    two following lines. Require both to be meaningful to avoid trivial
    //    matches such as duplicate blank lines. After finding the seed pair,
    //    greedily extend `k` while addedLines and `following` continue to
    //    match — only the verified-equal range is dropped.
    if (addedLines.length >= 3 && following.length >= 2 &&
        isMeaningfulLine(following[0]) && isMeaningfulLine(following[1])) {
        const a = following[0];
        const b = following[1];
        for (let start = 1; start < addedLines.length - 1; start++) {
            if (addedLines[start] !== a || addedLines[start + 1] !== b) {
                continue;
            }
            let k = 2;
            while (start + k < addedLines.length &&
                k < following.length &&
                addedLines[start + k] === following[k]) {
                k++;
            }
            return {
                kind: 'middle',
                newAdditions: [
                    ...addedLines.slice(0, start),
                    ...addedLines.slice(start + k),
                ],
                removedLines: addedLines.slice(start, start + k),
            };
        }
    }
    return undefined;
}
class XtabCustomDiffPatchResponseHandler {
    static async *handleResponse(linesStream, currentDocument, activeDocumentId, workspaceRoot, window, parentTracer, duplicateAdditionsMode = xtabPromptOptions_1.DuplicateAdditionsMode.Off, onDuplicateRemoved) {
        const tracer = parentTracer.createSubLogger(['XtabCustomDiffPatchResponseHandler', 'handleResponse']);
        const activeDocRelativePath = (0, promptCraftingUtils_1.toUniquePath)(activeDocumentId, workspaceRoot?.path);
        try {
            let dropAllRemaining = false;
            for await (const edit of XtabCustomDiffPatchResponseHandler.extractEdits(linesStream)) {
                if (dropAllRemaining) {
                    continue;
                }
                const isActiveDoc = edit.filePath === activeDocRelativePath;
                const targetDocument = isActiveDoc
                    ? activeDocumentId
                    : XtabCustomDiffPatchResponseHandler.resolveTargetDocument(edit.filePath, workspaceRoot);
                if (!targetDocument) {
                    tracer.error(`Could not resolve target document for edit: ${edit.toString()}`);
                    continue;
                }
                let lineReplacement = XtabCustomDiffPatchResponseHandler.resolveEdit(edit);
                // Only attempt dedup for the active document — other files'
                // content is not directly available here.
                if (duplicateAdditionsMode !== xtabPromptOptions_1.DuplicateAdditionsMode.Off && isActiveDoc) {
                    const removal = tryRemoveDuplicateAdditions(lineReplacement, currentDocument.content);
                    if (removal !== undefined) {
                        // Log only metadata (kind / counts / location) — do
                        // NOT include raw line content. The tracer output
                        // may end up in user-visible diagnostics.
                        tracer.trace(`Detected duplicate addition(s) (kind=${removal.kind}, count=${removal.removedLines.length}, mode=${duplicateAdditionsMode}) for edit at ${edit.filePath}:${edit.lineNumZeroBased}`);
                        onDuplicateRemoved?.({
                            summary: {
                                kind: removal.kind,
                                removedLineCount: removal.removedLines.length,
                                remainingAdditionCount: removal.newAdditions.length,
                            },
                            mode: duplicateAdditionsMode,
                            filePath: edit.filePath,
                            lineNumZeroBased: edit.lineNumZeroBased,
                        });
                        switch (duplicateAdditionsMode) {
                            case xtabPromptOptions_1.DuplicateAdditionsMode.Log:
                                // Yield the patch unchanged.
                                break;
                            case xtabPromptOptions_1.DuplicateAdditionsMode.DropPatch:
                                continue;
                            case xtabPromptOptions_1.DuplicateAdditionsMode.DropAllRemaining:
                                // Drop this patch and skip every subsequent
                                // patch in the stream. We still consume the
                                // stream to completion so the underlying
                                // fetch is allowed to finalize cleanly.
                                dropAllRemaining = true;
                                continue;
                            case xtabPromptOptions_1.DuplicateAdditionsMode.TrimDuplicate: {
                                const newAdditions = removal.newAdditions;
                                if (newAdditions.length === 0 && lineReplacement.lineRange.length === 0) {
                                    // Trim left a no-op patch — drop it.
                                    continue;
                                }
                                lineReplacement = new lineEdit_1.LineReplacement(lineReplacement.lineRange, newAdditions);
                                break;
                            }
                        }
                    }
                }
                yield {
                    edit: lineReplacement,
                    isFromCursorJump: false,
                    targetDocument,
                    window,
                };
            }
        }
        catch (e) {
            if (e instanceof fetchStreamError_1.FetchStreamError) {
                return e.reason;
            }
            const err = errors_1.ErrorUtils.fromUnknown(e);
            return new statelessNextEditProvider_1.NoNextEditReason.Unexpected(err);
        }
        return new statelessNextEditProvider_1.NoNextEditReason.NoSuggestions(currentDocument.content, window, undefined);
    }
    static resolveEdit(patch) {
        return new lineEdit_1.LineReplacement(new lineRange_1.LineRange(patch.lineNumZeroBased + 1, patch.lineNumZeroBased + 1 + patch.removedLines.length), patch.addedLines);
    }
    static resolveTargetDocument(filePath, workspaceRoot) {
        if ((0, path_1.isAbsolute)(filePath)) {
            return documentId_1.DocumentId.create(uri_1.URI.file(filePath).toString());
        }
        if (workspaceRoot) {
            return documentId_1.DocumentId.create(uri_1.URI.joinPath(workspaceRoot, filePath).toString());
        }
        // Relative path with no workspace root — cannot resolve to a valid URI
        return undefined;
    }
    static async *extractEdits(linesStream) {
        let currentPatch = null;
        for await (const line of linesStream) {
            // if no current patch, try to parse a new one
            if (line.trim() === tags_1.ResponseTags.NO_EDIT) {
                break;
            }
            if (currentPatch === null) {
                currentPatch = Patch.ofLine(line);
                continue;
            }
            // try to add line to current patch
            if (currentPatch.addLine(line)) {
                continue;
            }
            else { // line does not belong to current patch, yield current and start new
                if (currentPatch) {
                    yield currentPatch;
                }
                currentPatch = Patch.ofLine(line);
            }
        }
        if (currentPatch) {
            yield currentPatch;
        }
    }
}
exports.XtabCustomDiffPatchResponseHandler = XtabCustomDiffPatchResponseHandler;
//# sourceMappingURL=xtabCustomDiffPatchResponseHandler.js.map