"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var NextEditProvider_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NextEditFetchRequest = exports.NextEditProvider = exports.NesOutcome = void 0;
const path_1 = require("path");
const configurationService_1 = require("../../../platform/configuration/common/configurationService");
const edit_1 = require("../../../platform/inlineEdits/common/dataTypes/edit");
const rootedLineEdit_1 = require("../../../platform/inlineEdits/common/dataTypes/rootedLineEdit");
const xtabPromptOptions_1 = require("../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions");
const inlineEditLogContext_1 = require("../../../platform/inlineEdits/common/inlineEditLogContext");
const statelessNextEditProvider_1 = require("../../../platform/inlineEdits/common/statelessNextEditProvider");
const observable_1 = require("../../../platform/inlineEdits/common/utils/observable");
const logService_1 = require("../../../platform/log/common/logService");
const capturingToken_1 = require("../../../platform/requestLogger/common/capturingToken");
const requestLogger_1 = require("../../../platform/requestLogger/common/requestLogger");
const snippyService_1 = require("../../../platform/snippy/common/snippyService");
const nullExperimentationService_1 = require("../../../platform/telemetry/common/nullExperimentationService");
const errors_1 = require("../../../util/common/errors");
const result_1 = require("../../../util/common/result");
const assert_1 = require("../../../util/vs/base/common/assert");
const async_1 = require("../../../util/vs/base/common/async");
const cache_1 = require("../../../util/vs/base/common/cache");
const errors_2 = require("../../../util/vs/base/common/errors");
const lifecycle_1 = require("../../../util/vs/base/common/lifecycle");
const observable_2 = require("../../../util/vs/base/common/observable");
const stopwatch_1 = require("../../../util/vs/base/common/stopwatch");
const types_1 = require("../../../util/vs/base/common/types");
const uuid_1 = require("../../../util/vs/base/common/uuid");
const lineEdit_1 = require("../../../util/vs/editor/common/core/edits/lineEdit");
const stringEdit_1 = require("../../../util/vs/editor/common/core/edits/stringEdit");
const position_1 = require("../../../util/vs/editor/common/core/position");
const offsetRange_1 = require("../../../util/vs/editor/common/core/ranges/offsetRange");
const abstractText_1 = require("../../../util/vs/editor/common/core/text/abstractText");
const editRebase_1 = require("../common/editRebase");
const rejectionCollector_1 = require("../common/rejectionCollector");
const nextEditCache_1 = require("./nextEditCache");
const nextEditResult_1 = require("./nextEditResult");
const speculativeRequestManager_1 = require("./speculativeRequestManager");
/**
 * Computes a reduced window range that encompasses both the original window (shrunk by one line
 * on each end) and the full line where the cursor is located.
 *
 * This ensures the cache invalidation window always includes the cursor's line while trimming
 * the edges of the original window.
 */
function computeReducedWindow(window, activeDocSelection, documentBeforeEdits) {
    if (!activeDocSelection) {
        return window;
    }
    const cursorOffset = activeDocSelection.endExclusive;
    const t = documentBeforeEdits.getTransformer();
    const cursorPosition = t.getPosition(cursorOffset);
    const lineOffset = t.getOffset(cursorPosition.with(undefined, 1));
    const lineEndOffset = t.getOffset(cursorPosition.with(undefined, t.getLineLength(cursorPosition.lineNumber) + 1));
    const reducedOffset = t.getOffset(t.getPosition(window.start).delta(1));
    const reducedEndPosition = t.getPosition(window.endExclusive).delta(-2);
    const reducedEndOffset = t.getOffset(reducedEndPosition.column > 1 ? reducedEndPosition.with(undefined, t.getLineLength(reducedEndPosition.lineNumber) + 1) : reducedEndPosition);
    return new offsetRange_1.OffsetRange(Math.min(reducedOffset, lineOffset), Math.max(reducedEndOffset, lineEndOffset));
}
function convertLineEditToEdit(nextLineEdit, document) {
    const rootedLineEdit = new rootedLineEdit_1.RootedLineEdit(document, nextLineEdit);
    const suggestedEdit = rootedLineEdit.toEdit();
    // LineReplacement.toSingleTextEdit always joins newLines with '\n'.
    // If the document uses '\r\n' line endings, we need to match that in
    // the replacement text so that applying the edit produces consistent
    // line endings and the resulting content matches what VS Code reports.
    if (document.value.includes('\r\n')) {
        return new stringEdit_1.StringEdit(suggestedEdit.replacements.map(r => new stringEdit_1.StringReplacement(r.replaceRange, r.newText.replace(/\n/g, '\r\n'))));
    }
    return suggestedEdit;
}
function createDocStateLookupMap(projectedDocuments, xtabEditHistory) {
    const statePerDoc = new cache_1.CachedFunction((id) => {
        const doc = projectedDocuments.find(d => d.nextEditDoc.id === id);
        if (!doc) {
            for (let i = xtabEditHistory.length - 1; i >= 0; i--) {
                const entry = xtabEditHistory[i];
                if (entry.docId === id && entry.kind === 'edit') {
                    const baseDocState = entry.edit.getEditedState();
                    return {
                        baseDocState,
                        docContents: baseDocState,
                        editsSoFar: stringEdit_1.StringEdit.empty,
                        nextEdits: [],
                        docId: id,
                    };
                }
            }
            throw new errors_2.BugIndicatingError();
        }
        return {
            baseDocState: doc.documentAfterEdits,
            docContents: doc.documentAfterEdits,
            editsSoFar: stringEdit_1.StringEdit.empty,
            nextEdits: [],
            docId: id,
        };
    });
    return statePerDoc;
}
var NesOutcome;
(function (NesOutcome) {
    NesOutcome["Accepted"] = "accepted";
    NesOutcome["Rejected"] = "rejected";
    NesOutcome["Ignored"] = "ignored";
})(NesOutcome || (exports.NesOutcome = NesOutcome = {}));
let NextEditProvider = NextEditProvider_1 = class NextEditProvider extends lifecycle_1.Disposable {
    get lastRejectionTime() {
        return this._lastRejectionTime;
    }
    get lastTriggerTime() {
        return this._lastTriggerTime;
    }
    get lastOutcome() {
        return this._lastOutcome;
    }
    constructor(_workspace, _statelessNextEditProvider, _historyContextProvider, _xtabHistoryTracker, _debugRecorder, _configService, _snippyService, _logService, _expService, _requestLogger) {
        super();
        this._workspace = _workspace;
        this._statelessNextEditProvider = _statelessNextEditProvider;
        this._historyContextProvider = _historyContextProvider;
        this._xtabHistoryTracker = _xtabHistoryTracker;
        this._debugRecorder = _debugRecorder;
        this._configService = _configService;
        this._snippyService = _snippyService;
        this._logService = _logService;
        this._expService = _expService;
        this._requestLogger = _requestLogger;
        this.ID = this._statelessNextEditProvider.ID;
        this._rejectionCollector = this._register(new rejectionCollector_1.RejectionCollector(this._workspace, this._logService));
        this._pendingStatelessNextEditRequest = null;
        this._lastShownTime = 0;
        /** The requestId of the last shown suggestion. We store only the requestId (not the object) to avoid preventing garbage collection. */
        this._lastShownSuggestionId = undefined;
        this._lastRejectionTime = 0;
        this._lastTriggerTime = 0;
        this._shouldExpandEditWindow = false;
        this._logger = this._logService.createSubLogger(['NES', 'NextEditProvider']);
        this._nextEditCache = new nextEditCache_1.NextEditCache(this._workspace, this._logService, this._configService, this._expService);
        this._specManager = this._register(new speculativeRequestManager_1.SpeculativeRequestManager(this._logger.createSubLogger('SpeculativeRequestManager')));
        (0, observable_2.mapObservableArrayCached)(this, this._workspace.openDocuments, (doc, store) => {
            store.add((0, observable_2.runOnChange)(doc.value, (value) => {
                this._cancelPendingRequestDueToDocChange(doc.id, value);
                // FIXME: don't invoke before fixing false positive cancellations
                // this._specManager.onActiveDocumentChanged(doc.id, value.value);
            }));
            // When the per-doc store is disposed, the document was removed from
            // openDocuments. Cancel any speculative targeting it — its cached result
            // would never be hit again.
            store.add((0, lifecycle_1.toDisposable)(() => this._specManager.onDocumentClosed(doc.id)));
        }).recomputeInitiallyAndOnChange(this._store);
    }
    /**
     * Cancels the in-flight stateless next-edit request when the document it
     * was issued for has diverged from the request's expected post-edit state.
     *
     * Invoked from the per-document `runOnChange` autorun in the constructor
     * whenever an open document's value changes. The pending request was built
     * against a specific snapshot (`documentAfterEdits`); if the user has since
     * typed something that makes the current value differ from that snapshot,
     * the result would no longer be applicable and is cancelled eagerly.
     *
     * Skipped when:
     * - the `InlineEditsAsyncCompletions` experiment is enabled (that path
     *   tolerates divergence and rebases later), or
     * - there is no pending request, or
     * - the changed document is not the one the pending request targets.
     *
     * Note: this only handles the regular pending stateless request. Speculative
     * requests have their own divergence handling via
     * `SpeculativeRequestManager.onActiveDocumentChanged` (trajectory check).
     */
    _cancelPendingRequestDueToDocChange(docId, docValue) {
        const isAsyncCompletions = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsAsyncCompletions, this._expService);
        if (isAsyncCompletions || this._pendingStatelessNextEditRequest === null) {
            return;
        }
        const activeDoc = this._pendingStatelessNextEditRequest.getActiveDocument();
        if (activeDoc.id === docId && activeDoc.documentAfterEdits.value !== docValue.value) {
            this._pendingStatelessNextEditRequest.cancellationTokenSource.cancel();
        }
    }
    async getNextEdit(docId, context, logContext, cancellationToken, telemetryBuilder) {
        const now = Date.now();
        this._lastTriggerTime = now;
        const sw = new stopwatch_1.StopWatch();
        const logger = this._logger.createSubLogger(context.requestUuid.substring(4, 8))
            .withExtraTarget(logService_1.LogTarget.fromCallback((_level, msg) => {
            logContext.trace(`[${Math.floor(sw.elapsed()).toString().padStart(4, ' ')}ms] ${msg}`);
        }));
        const shouldExpandEditWindow = this._shouldExpandEditWindow;
        logContext.setStatelessNextEditProviderId(this._statelessNextEditProvider.ID);
        let result;
        try {
            result = await this._getNextEditCanThrow(docId, context, now, shouldExpandEditWindow, logger, logContext, cancellationToken, telemetryBuilder);
        }
        catch (error) {
            logContext.setError(error);
            telemetryBuilder.setNextEditProviderError(errors_1.ErrorUtils.toString(error));
            throw error;
        }
        finally {
            telemetryBuilder.markEndTime();
        }
        this._lastNextEditResult = result;
        return result;
    }
    async _getNextEditCanThrow(docId, context, triggerTime, shouldExpandEditWindow, parentLogger, logContext, cancellationToken, telemetryBuilder) {
        const logger = parentLogger.createSubLogger('_getNextEdit');
        logger.trace(`invoked with trigger id = ${context.changeHint === undefined ? 'undefined' : `uuid = ${context.changeHint.data.uuid}, reason = ${context.changeHint.data.reason}`}`);
        const doc = this._workspace.getDocument(docId);
        if (!doc) {
            logger.trace(`Document "${docId.baseName}" not found`);
            throw new errors_2.BugIndicatingError(`Document "${docId.baseName}" not found`);
        }
        const documentAtInvocationTime = doc.value.get();
        const selections = doc.selection.get();
        const nesConfigs = this.determineNesConfigs(telemetryBuilder, logContext);
        const cachedEdit = this._nextEditCache.lookupNextEdit(docId, documentAtInvocationTime, selections);
        if (cachedEdit?.rejected) {
            logger.trace('cached edit was previously rejected');
            telemetryBuilder.setStatus('previouslyRejectedCache');
            telemetryBuilder.setWasPreviouslyRejected();
            logContext.markAsPreviouslyRejected();
            const rejectedEdit = cachedEdit.rebasedEdit ?? cachedEdit.edit;
            if (rejectedEdit) {
                this._rejectionCollector.reject(docId, rejectedEdit);
            }
            const nextEditResult = new nextEditResult_1.NextEditResult(logContext.requestId, cachedEdit.source, undefined);
            return nextEditResult;
        }
        let edit;
        let currentDocument;
        let error;
        let req;
        let targetDocumentId = docId;
        let isRebasedCachedEdit = false;
        let isSubsequentCachedEdit = false;
        let isFromSpeculativeRequest = false;
        let cacheEntry;
        if (cachedEdit) {
            logger.trace('using cached edit');
            const actualEdit = cachedEdit.rebasedEdit || cachedEdit.edit;
            if (actualEdit) {
                edit = { actualEdit, isFromCursorJump: cachedEdit.isFromCursorJump };
            }
            isRebasedCachedEdit = !!cachedEdit.rebasedEdit;
            isSubsequentCachedEdit = cachedEdit.subsequentN !== undefined && cachedEdit.subsequentN > 0;
            isFromSpeculativeRequest = cachedEdit.source.isSpeculative;
            req = cachedEdit.source;
            logContext.setIsCachedResult(cachedEdit.source.log);
            currentDocument = documentAtInvocationTime;
            telemetryBuilder.setHeaderRequestId(req.headerRequestId);
            telemetryBuilder.setIsFromCache();
            telemetryBuilder.setSubsequentEditOrder(cachedEdit.rebasedEditIndex ?? cachedEdit.subsequentN);
            // back-date the recording bookmark of the cached edit to the bookmark of the original request.
            logContext.recordingBookmark = req.log.recordingBookmark;
            cacheEntry = cachedEdit.baseCacheEntry ?? cachedEdit;
        }
        else {
            logger.trace(`fetching next edit with shouldExpandEditWindow=${shouldExpandEditWindow}`);
            const providerRequestStartDateTime = (this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsDebounceUseCoreRequestTime, this._expService)
                ? (context.requestIssuedDateTime ?? undefined)
                : undefined);
            req = new NextEditFetchRequest(context.requestUuid, logContext, providerRequestStartDateTime, false);
            telemetryBuilder.setHeaderRequestId(req.headerRequestId);
            const startVersion = doc.value.get();
            logger.trace('awaiting firstEdit promise');
            const result = await this.fetchNextEdit(req, doc, nesConfigs, shouldExpandEditWindow, logger, telemetryBuilder, cancellationToken);
            logger.trace('resolved firstEdit promise');
            const latency = `First edit latency: ${Date.now() - this._lastTriggerTime} ms`;
            logContext.addLog(latency);
            logger.trace(latency);
            if (result.isError()) {
                logger.trace(`failed to fetch next edit ${result.err.toString()}`);
                telemetryBuilder.setStatus(`noEdit:${result.err.kind}`);
                error = result.err;
            }
            else {
                targetDocumentId = result.val.docId ?? targetDocumentId;
                const targetDoc = targetDocumentId ? this._workspace.getDocument(targetDocumentId) : doc;
                currentDocument = targetDoc.value.get();
                const docDidChange = targetDocumentId === doc.id && startVersion.value !== currentDocument.value;
                if (docDidChange) {
                    logger.trace('document changed while fetching next edit');
                    telemetryBuilder.setStatus('docChanged');
                    logContext.setIsSkipped();
                }
                else {
                    const suggestedNextEdit = result.val.rebasedEdit || result.val.edit;
                    if (!suggestedNextEdit) {
                        logger.trace('empty edits');
                        telemetryBuilder.setStatus('emptyEdits');
                    }
                    else {
                        logger.trace('fetch succeeded');
                        logContext.setResponseResults([suggestedNextEdit]); // TODO: other streamed edits?
                        edit = { actualEdit: suggestedNextEdit, isFromCursorJump: result.val.isFromCursorJump };
                        isFromSpeculativeRequest = result.val.isFromSpeculativeRequest ?? false;
                        cacheEntry = result.val.baseCacheEntry ?? result.val;
                    }
                }
            }
        }
        if (error instanceof statelessNextEditProvider_1.NoNextEditReason.FetchFailure || error instanceof statelessNextEditProvider_1.NoNextEditReason.Unexpected) {
            logger.trace(`has throwing error: ${error.error}`);
            throw error.error;
        }
        else if (error instanceof statelessNextEditProvider_1.NoNextEditReason.NoSuggestions) {
            if (error.nextCursorPosition === undefined) {
                logContext.markAsNoSuggestions();
            }
            else {
                telemetryBuilder.setStatus('emptyEditsButHasNextCursorPosition');
                return new nextEditResult_1.NextEditResult(logContext.requestId, req, { jumpToPosition: error.nextCursorPosition, targetDocumentId: error.nextCursorDocumentId, documentBeforeEdits: documentAtInvocationTime, isFromCursorJump: false, isSubsequentEdit: false });
            }
        }
        else if (error instanceof statelessNextEditProvider_1.NoNextEditReason.GotCancelled) {
            logContext.setIsSkipped();
        }
        const emptyResult = new nextEditResult_1.NextEditResult(logContext.requestId, req, undefined);
        if (!edit) {
            logger.trace('had no edit');
            // telemetry builder status must've been set earlier
            return emptyResult;
        }
        if (cancellationToken.isCancellationRequested) {
            logger.trace('cancelled');
            telemetryBuilder.setStatus(`noEdit:gotCancelled`);
            return emptyResult;
        }
        if (this._rejectionCollector.isRejected(targetDocumentId, edit.actualEdit) || currentDocument && this._nextEditCache.isRejectedNextEdit(targetDocumentId, currentDocument, edit.actualEdit)) {
            logger.trace('edit was previously rejected');
            telemetryBuilder.setStatus('previouslyRejected');
            telemetryBuilder.setWasPreviouslyRejected();
            logContext.markAsPreviouslyRejected();
            return emptyResult;
        }
        logContext.setResult(rootedLineEdit_1.RootedLineEdit.fromEdit(new edit_1.RootedEdit(documentAtInvocationTime, new stringEdit_1.StringEdit([edit.actualEdit]))));
        (0, assert_1.assert)(currentDocument !== undefined, 'should be defined if edit is defined');
        telemetryBuilder.setStatus('notAccepted'); // Acceptance pending.
        const nextEditResult = new nextEditResult_1.NextEditResult(logContext.requestId, req, { edit: edit.actualEdit, isFromCursorJump: edit.isFromCursorJump, documentBeforeEdits: currentDocument, targetDocumentId, isSubsequentEdit: isSubsequentCachedEdit, cacheEntry });
        telemetryBuilder.setHasNextEdit(true);
        const delay = this.computeMinimumResponseDelay({ triggerTime, isRebasedCachedEdit, isSubsequentCachedEdit, isFromSpeculativeRequest, enforceCacheDelay: context.enforceCacheDelay }, logger);
        if (delay > 0) {
            await (0, async_1.timeout)(delay);
            if (cancellationToken.isCancellationRequested) {
                logger.trace('cancelled');
                telemetryBuilder.setStatus(`noEdit:gotCancelled`);
                return emptyResult;
            }
        }
        logger.trace('returning next edit result');
        return nextEditResult;
    }
    determineNesConfigs(telemetryBuilder, logContext) {
        const nesConfigs = {
            isAsyncCompletions: this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsAsyncCompletions, this._expService),
            isEagerBackupRequest: this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsEagerBackupRequest, this._expService),
        };
        telemetryBuilder.setNESConfigs({ ...nesConfigs });
        logContext.addCodeblockToLog(JSON.stringify(nesConfigs, null, '\t'));
        return nesConfigs;
    }
    _processDoc(doc) {
        const documentLinesBeforeEdit = doc.lastEdit.base.getLines();
        const recentEdits = doc.lastEdits;
        const recentEdit = rootedLineEdit_1.RootedLineEdit.fromEdit(new edit_1.RootedEdit(doc.lastEdit.base, doc.lastEdits.compose())).removeCommonSuffixPrefixLines().edit;
        const documentBeforeEdits = doc.lastEdit.base;
        const lastSelectionInAfterEdits = doc.lastSelection;
        const workspaceRoot = this._workspace.getWorkspaceRoot(doc.docId);
        const nextEditDoc = new statelessNextEditProvider_1.StatelessNextEditDocument(doc.docId, workspaceRoot, doc.languageId, documentLinesBeforeEdit, recentEdit, documentBeforeEdits, recentEdits, lastSelectionInAfterEdits);
        return {
            recentEdit: doc.lastEdit,
            nextEditDoc,
            documentAfterEdits: nextEditDoc.documentAfterEdits,
        };
    }
    async fetchNextEdit(req, doc, nesConfigs, shouldExpandEditWindow, parentLogger, telemetryBuilder, cancellationToken) {
        const curDocId = doc.id;
        const logger = parentLogger.createSubLogger('fetchNextEdit');
        const historyContext = this._historyContextProvider.getHistoryContext(curDocId);
        if (!historyContext) {
            return result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.Unexpected(new Error('DocumentMissingInHistoryContext')));
        }
        const documentAtInvocationTime = doc.value.get();
        const selectionAtInvocationTime = doc.selection.get();
        const logContext = req.log;
        logContext.setRecentEdit(historyContext);
        const cursorAtInvocationTime = selectionAtInvocationTime.at(0);
        const cursorInRequestEditWindow = (request) => !request.requestEditWindow || !cursorAtInvocationTime || request.requestEditWindow.containsCursor(cursorAtInvocationTime);
        // Check if we can reuse the regular pending request
        const pendingRequestStillCurrent = documentAtInvocationTime.value === this._pendingStatelessNextEditRequest?.documentBeforeEdits.value;
        const cursorWithinPendingEditWindow = !this._pendingStatelessNextEditRequest || cursorInRequestEditWindow(this._pendingStatelessNextEditRequest);
        const existingNextEditRequest = (pendingRequestStillCurrent || nesConfigs.isAsyncCompletions) && cursorWithinPendingEditWindow
            && !this._pendingStatelessNextEditRequest?.cancellationTokenSource.token.isCancellationRequested
            && this._pendingStatelessNextEditRequest || undefined;
        // Check if we can reuse the speculative pending request (from when a suggestion was shown)
        const specPending = this._specManager.pending;
        const speculativeRequestMatches = specPending?.docId === curDocId
            && specPending?.postEditContent === documentAtInvocationTime.value
            && !specPending.request.cancellationTokenSource.token.isCancellationRequested
            && cursorInRequestEditWindow(specPending.request);
        const speculativeRequest = speculativeRequestMatches ? specPending?.request : undefined;
        // Prefer speculative request if it matches (it was specifically created for this post-edit state)
        const requestToReuse = speculativeRequest ?? existingNextEditRequest;
        if (requestToReuse) {
            // Nice! No need to make another request, we can reuse the result from a pending request.
            if (speculativeRequest) {
                logger.trace(`reusing speculative pending request (opportunityId=${speculativeRequest.opportunityId}, headerRequestId=${speculativeRequest.headerRequestId})`);
                // Detach the speculative — caller is consuming it now.
                this._specManager.consumePending();
            }
            else {
                logger.trace(`reusing in-flight pending request (opportunityId=${requestToReuse.opportunityId}, headerRequestId=${requestToReuse.headerRequestId})`);
            }
            const requestStillCurrent = speculativeRequest
                ? speculativeRequestMatches // For speculative, we already checked it matches
                : pendingRequestStillCurrent;
            const reusedRequestKind = speculativeRequest ? "speculative" /* ReusedRequestKind.Speculative */ : "async" /* ReusedRequestKind.Async */;
            if (requestStillCurrent) {
                const nextEditResult = await this._joinNextEditRequest(requestToReuse, reusedRequestKind, telemetryBuilder, logContext, cancellationToken);
                telemetryBuilder.setStatelessNextEditTelemetry(nextEditResult.telemetry);
                if (speculativeRequest) {
                    const firstEdit = await requestToReuse.firstEdit.p;
                    return firstEdit.map(val => ({ ...val, isFromSpeculativeRequest: true }));
                }
                return nextEditResult.nextEdit.isError() ? nextEditResult.nextEdit : requestToReuse.firstEdit.p;
            }
            else if (nesConfigs.isEagerBackupRequest) {
                // The pending request is stale (document diverged). Start a backup request
                // in parallel so that if rebase fails, we already have a head start.
                logger.trace('starting eager backup request in parallel with rebase attempt');
                // _executeNewNextEditRequest cancels the current _pendingStatelessNextEditRequest,
                // but we're still trying to join+rebase requestToReuse. Temporarily clear the
                // pending field so the stale request isn't cancelled prematurely.
                this._pendingStatelessNextEditRequest = null;
                const backupPromise = this._executeNewNextEditRequest(req, doc, historyContext, nesConfigs, shouldExpandEditWindow, logger, telemetryBuilder, cancellationToken);
                const cancelBackupRequest = () => {
                    void backupPromise
                        .then(r => r.nextEditRequest.cancellationTokenSource.cancel())
                        .catch(() => undefined);
                };
                // Simultaneously attempt to join + rebase the stale request
                const nextEditResult = await this._joinNextEditRequest(requestToReuse, reusedRequestKind, telemetryBuilder, logContext, cancellationToken);
                const cacheResult = await requestToReuse.firstEdit.p;
                if (cacheResult.isOk() && cacheResult.val.edit) {
                    const rebaseResult = this._nextEditCache.tryRebaseCacheEntry(cacheResult.val, documentAtInvocationTime, selectionAtInvocationTime);
                    if (rebaseResult.edit) {
                        logger.trace('rebase succeeded, cancelling eager backup request');
                        cancelBackupRequest();
                        telemetryBuilder.setStatelessNextEditTelemetry(nextEditResult.telemetry);
                        return result_1.Result.ok(rebaseResult.edit);
                    }
                    this._logRebaseFailure(rebaseResult.failureInfo, logContext);
                }
                if (cancellationToken.isCancellationRequested) {
                    logger.trace('cancelled after rebase failed (eager backup path)');
                    cancelBackupRequest();
                    telemetryBuilder.setStatelessNextEditTelemetry(nextEditResult.telemetry);
                    return result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.GotCancelled('afterFailedRebase'));
                }
                // Rebase failed — use the backup request that's already been running in parallel
                logger.trace('rebase failed, using eager backup request');
                const backupRes = await backupPromise;
                telemetryBuilder.setStatelessNextEditTelemetry(backupRes.nextEditResult.telemetry);
                return backupRes.nextEditResult.nextEdit.isError() ? backupRes.nextEditResult.nextEdit : backupRes.nextEditRequest.firstEdit.p;
            }
            else {
                const nextEditResult = await this._joinNextEditRequest(requestToReuse, reusedRequestKind, telemetryBuilder, logContext, cancellationToken);
                // Needs rebasing.
                const cacheResult = await requestToReuse.firstEdit.p;
                if (cacheResult.isOk() && cacheResult.val.edit) {
                    const rebaseResult = this._nextEditCache.tryRebaseCacheEntry(cacheResult.val, documentAtInvocationTime, selectionAtInvocationTime);
                    if (rebaseResult.edit) {
                        telemetryBuilder.setStatelessNextEditTelemetry(nextEditResult.telemetry);
                        return result_1.Result.ok(rebaseResult.edit);
                    }
                    this._logRebaseFailure(rebaseResult.failureInfo, logContext);
                }
                if (cancellationToken.isCancellationRequested) {
                    logger.trace('document changed after rebase failed');
                    telemetryBuilder.setStatelessNextEditTelemetry(nextEditResult.telemetry);
                    return result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.GotCancelled('afterFailedRebase'));
                }
                // Rebase failed (or result had error). Check if there is a new pending request. Otherwise continue with a new request below.
                const pendingRequestStillCurrent2 = documentAtInvocationTime.value === this._pendingStatelessNextEditRequest?.documentBeforeEdits.value;
                const existingNextEditRequest2 = pendingRequestStillCurrent2 && !this._pendingStatelessNextEditRequest?.cancellationTokenSource.token.isCancellationRequested
                    && this._pendingStatelessNextEditRequest || undefined;
                if (existingNextEditRequest2) {
                    logger.trace('reusing 2nd existing next edit request after rebase failed');
                    const nextEditResult2 = await this._joinNextEditRequest(existingNextEditRequest2, "async" /* ReusedRequestKind.Async */, telemetryBuilder, logContext, cancellationToken);
                    telemetryBuilder.setStatelessNextEditTelemetry(nextEditResult2.telemetry);
                    return nextEditResult2.nextEdit.isError() ? nextEditResult2.nextEdit : existingNextEditRequest2.firstEdit.p;
                }
                logger.trace('creating new next edit request after rebase failed');
            }
        }
        const res = await this._executeNewNextEditRequest(req, doc, historyContext, nesConfigs, shouldExpandEditWindow, logger, telemetryBuilder, cancellationToken);
        const nextEditRequest = res.nextEditRequest;
        const nextEditResult = res.nextEditResult;
        telemetryBuilder.setStatelessNextEditTelemetry(nextEditResult.telemetry);
        return nextEditResult.nextEdit.isError() ? nextEditResult.nextEdit : nextEditRequest.firstEdit.p;
    }
    async _joinNextEditRequest(nextEditRequest, reusedRequestKind, telemetryBuilder, logContext, cancellationToken) {
        telemetryBuilder.setHeaderRequestId(nextEditRequest.headerRequestId);
        telemetryBuilder.setReusedRequest(reusedRequestKind);
        telemetryBuilder.setRequest(nextEditRequest);
        logContext.setRequestInput(nextEditRequest);
        logContext.setIsReusedInFlightResult(nextEditRequest.logContext);
        const disp = this._hookupCancellation(nextEditRequest, cancellationToken);
        try {
            return await nextEditRequest.result;
        }
        finally {
            disp.dispose();
        }
    }
    _logRebaseFailure(failureInfo, logContext) {
        if (failureInfo) {
            logContext.setRebaseFailure(failureInfo);
        }
    }
    async _executeNewNextEditRequest(req, doc, historyContext, nesConfigs, shouldExpandEditWindow, parentLogger, telemetryBuilder, cancellationToken) {
        const curDocId = doc.id;
        const logger = parentLogger.createSubLogger('_executeNewNextEditRequest');
        const recording = this._debugRecorder?.getRecentLog();
        const logContext = req.log;
        const activeDocAndIdx = assertDefined(historyContext.getDocumentAndIdx(curDocId));
        const activeDocSelection = doc.selection.get()[0];
        const projectedDocuments = historyContext.documents.map(doc => this._processDoc(doc));
        const xtabEditHistory = this._xtabHistoryTracker.getHistory();
        const firstEdit = new async_1.DeferredPromise();
        const nLinesEditWindow = (shouldExpandEditWindow
            ? this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsAutoExpandEditWindowLines, this._expService)
            : undefined);
        const nextEditRequest = new statelessNextEditProvider_1.StatelessNextEditRequest(req.headerRequestId, req.opportunityId, doc.value.get(), projectedDocuments.map(d => d.nextEditDoc), activeDocAndIdx.idx, xtabEditHistory, firstEdit, nLinesEditWindow, false, // isSpeculative
        logContext, req.log.recordingBookmark, recording, req.providerRequestStartDateTime);
        let nextEditResult;
        if (this._pendingStatelessNextEditRequest) {
            this._pendingStatelessNextEditRequest.cancellationTokenSource.cancel();
            this._pendingStatelessNextEditRequest = null;
            // Clear any scheduled (but not yet triggered) speculative request tied to the
            // old stream — it would otherwise fire stale when the old stream's background
            // loop calls handleStreamEnd after the stream has already been superseded.
            this._specManager.clearScheduled();
        }
        // Cancel speculative request if it doesn't match the document/state
        // of this new request — it was built for a different document or post-edit state.
        this._specManager.cancelIfMismatch(curDocId, nextEditRequest.documentBeforeEdits.value, "superseded" /* SpeculativeCancelReason.Superseded */);
        this._pendingStatelessNextEditRequest = nextEditRequest;
        const removeFromPending = () => {
            if (this._pendingStatelessNextEditRequest === nextEditRequest) {
                this._pendingStatelessNextEditRequest = null;
            }
        };
        telemetryBuilder.setRequest(nextEditRequest);
        telemetryBuilder.setStatus('requested');
        logContext.setRequestInput(nextEditRequest);
        // A note on cancellation:
        //
        // We don't cancel when the cancellation token is signalled, because we have our own
        // separate cancellation logic which ends up cancelling based on documents changing.
        //
        // But we do cancel requests which didn't start yet if no-one really needs their result
        //
        const disp = this._hookupCancellation(nextEditRequest, cancellationToken, nesConfigs.isAsyncCompletions ? (0, observable_1.autorunWithChanges)(this, {
            value: doc.value,
        }, data => {
            data.value.changes.forEach(edit => {
                if (nextEditRequest.intermediateUserEdit && !edit.isEmpty()) {
                    nextEditRequest.intermediateUserEdit = nextEditRequest.intermediateUserEdit.compose(edit);
                    if (!(0, editRebase_1.checkEditConsistency)(nextEditRequest.documentBeforeEdits.value, nextEditRequest.intermediateUserEdit, data.value.value.value, logger)) {
                        nextEditRequest.intermediateUserEdit = undefined;
                    }
                }
            });
        }) : undefined);
        const statePerDoc = createDocStateLookupMap(projectedDocuments, xtabEditHistory);
        const editStream = this._statelessNextEditProvider.provideNextEdit(nextEditRequest, logger, logContext, nextEditRequest.cancellationTokenSource.token);
        let ithEdit = -1;
        const processEdit = (streamedEdit, telemetry) => {
            ++ithEdit;
            const myLogger = logger.createSubLogger('processEdit');
            myLogger.trace(`processing edit #${ithEdit} (starts at 0)`);
            // reset shouldExpandEditWindow to false when we get any edit
            myLogger.trace('resetting shouldExpandEditWindow to false due to receiving an edit');
            this._shouldExpandEditWindow = false;
            const targetDocState = statePerDoc.get(streamedEdit.targetDocument ?? curDocId);
            const singleLineEdit = streamedEdit.edit;
            const lineEdit = new lineEdit_1.LineEdit([singleLineEdit]);
            const edit = convertLineEditToEdit(lineEdit, targetDocState.baseDocState);
            const rebasedEdit = edit.tryRebase(targetDocState.editsSoFar);
            if (rebasedEdit === undefined) {
                myLogger.trace(`edit ${ithEdit} is undefined after rebasing`);
                if (!firstEdit.isSettled) {
                    firstEdit.complete(result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.Uncategorized(new Error('Rebased edit is undefined'))));
                }
                return undefined;
            }
            targetDocState.editsSoFar = targetDocState.editsSoFar.compose(rebasedEdit);
            let cachedEdit;
            if (rebasedEdit.replacements.length === 0) {
                myLogger.trace(`WARNING: ${ithEdit} has no edits`);
            }
            else if (rebasedEdit.replacements.length > 1) {
                myLogger.trace(`WARNING: ${ithEdit} has ${rebasedEdit.replacements.length} edits, but expected only 1`);
            }
            else {
                // populate the cache
                const nextEditReplacement = rebasedEdit.replacements[0];
                targetDocState.nextEdits.push(nextEditReplacement);
                cachedEdit = this._nextEditCache.setKthNextEdit(targetDocState.docId, targetDocState.docContents, ithEdit === 0 ? streamedEdit.window : undefined, nextEditReplacement, ithEdit, ithEdit === 0 ? targetDocState.nextEdits : undefined, ithEdit === 0 ? nextEditRequest.intermediateUserEdit : undefined, req, { isFromCursorJump: streamedEdit.isFromCursorJump, originalEditWindow: streamedEdit.originalWindow, cursorOffset: targetDocState.docId === curDocId ? activeDocSelection?.start : undefined });
                myLogger.trace(`populated cache for ${ithEdit}`);
            }
            if (!firstEdit.isSettled) {
                myLogger.trace('resolving firstEdit promise');
                logContext.setResult(new rootedLineEdit_1.RootedLineEdit(targetDocState.docContents, lineEdit)); // this's correct without rebasing because this's the first edit
                firstEdit.complete(cachedEdit ? result_1.Result.ok(cachedEdit) : result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.Unexpected(new Error('No cached edit'))));
            }
            targetDocState.docContents = rebasedEdit.applyOnText(targetDocState.docContents);
            return cachedEdit;
        };
        const handleStreamEnd = (completionReason, lastTelemetry) => {
            const myLogger = logger.createSubLogger('streamEnd');
            // if there was a request made, and it ended without any edits, reset shouldExpandEditWindow
            const hadNoEdits = ithEdit === -1;
            if (hadNoEdits && completionReason instanceof statelessNextEditProvider_1.NoNextEditReason.NoSuggestions) {
                myLogger.trace('resetting shouldExpandEditWindow to false due to NoSuggestions');
                this._shouldExpandEditWindow = false;
            }
            if (statePerDoc.get(curDocId).nextEdits.length) {
                myLogger.trace(`${statePerDoc.get(curDocId).nextEdits.length} edits returned`);
            }
            else {
                myLogger.trace(`no edit, reason: ${completionReason.kind}`);
                if (completionReason instanceof statelessNextEditProvider_1.NoNextEditReason.NoSuggestions) {
                    const { documentBeforeEdits, window } = completionReason;
                    const reducedWindow = window ? computeReducedWindow(window, activeDocSelection, documentBeforeEdits) : undefined;
                    this._nextEditCache.setNoNextEdit(curDocId, documentBeforeEdits, reducedWindow, req);
                }
            }
            if (!firstEdit.isSettled) {
                firstEdit.complete(result_1.Result.error(completionReason));
            }
            const resultForTelemetry = statePerDoc.get(curDocId).nextEdits.length > 0
                ? result_1.Result.ok(undefined)
                : result_1.Result.error(completionReason);
            const result = new statelessNextEditProvider_1.StatelessNextEditResult(resultForTelemetry, lastTelemetry);
            nextEditRequest.setResult(result);
            disp.dispose();
            removeFromPending();
            // Fire any scheduled speculative request — the last shown edit
            // was indeed the last edit from this stream.
            const scheduled = this._specManager.consumeScheduled(nextEditRequest.headerRequestId);
            if (scheduled) {
                void this._triggerSpeculativeRequest(scheduled.suggestion);
            }
            return result;
        };
        try {
            let res = await editStream.next();
            if (res.done) {
                // Stream ended immediately without any edits
                const completionReason = res.value.v;
                nextEditResult = handleStreamEnd(completionReason, res.value.telemetryBuilder);
            }
            else {
                // Process first edit synchronously
                const firstStreamedEdit = res.value.v;
                const firstTelemetry = res.value.telemetryBuilder;
                processEdit(firstStreamedEdit, firstTelemetry);
                // Continue streaming remaining edits in the background (unawaited)
                (async () => {
                    try {
                        res = await editStream.next();
                        while (!res.done) {
                            const streamedEdit = res.value.v;
                            processEdit(streamedEdit, res.value.telemetryBuilder);
                            // A new edit arrived from the stream — the previously-shown
                            // edit was not the last one. Clear the scheduled speculative.
                            this._specManager.consumeScheduled(nextEditRequest.headerRequestId);
                            res = await editStream.next();
                        }
                        // Stream completed
                        const completionReason = res.value.v;
                        handleStreamEnd(completionReason, res.value.telemetryBuilder);
                    }
                    catch (err) {
                        logger.trace(`Error while streaming further edits: ${errors_1.ErrorUtils.toString(err)}`);
                        const errorReason = new statelessNextEditProvider_1.NoNextEditReason.Unexpected(errors_1.ErrorUtils.fromUnknown(err));
                        handleStreamEnd(errorReason, firstTelemetry);
                    }
                })();
                nextEditResult = new statelessNextEditProvider_1.StatelessNextEditResult(result_1.Result.ok(undefined), firstTelemetry);
            }
        }
        catch (err) {
            nextEditRequest.setResultError(err);
            throw err;
        }
        return { nextEditRequest, nextEditResult };
    }
    _hookupCancellation(nextEditRequest, cancellationToken, attachedDisposable) {
        const disposables = new lifecycle_1.DisposableStore();
        let dependantRemoved = false;
        const removeDependant = () => {
            if (!dependantRemoved) {
                dependantRemoved = true;
                nextEditRequest.liveDependentants--;
            }
        };
        const cancellationTimer = disposables.add(new async_1.TimeoutTimer());
        disposables.add(cancellationToken.onCancellationRequested(() => {
            removeDependant();
            if (nextEditRequest.liveDependentants > 0) {
                // there are others depending on this request
                return;
            }
            if (!nextEditRequest.fetchIssued) {
                // fetch not issued => cancel!
                nextEditRequest.cancellationTokenSource.cancel();
                attachedDisposable?.dispose();
                return;
            }
            cancellationTimer.setIfNotSet(() => {
                if (nextEditRequest.liveDependentants > 0) {
                    // there are others depending on this request
                    return;
                }
                nextEditRequest.cancellationTokenSource.cancel();
                attachedDisposable?.dispose();
            }, 1000); // This needs to be longer than the pause between two requests from Core otherwise we cancel running requests too early.
        }));
        disposables.add((0, lifecycle_1.toDisposable)(() => {
            removeDependant();
            if (nextEditRequest.liveDependentants === 0) {
                attachedDisposable?.dispose();
            }
        }));
        nextEditRequest.liveDependentants++;
        return disposables;
    }
    computeMinimumResponseDelay({ triggerTime, isRebasedCachedEdit, isSubsequentCachedEdit, isFromSpeculativeRequest, enforceCacheDelay }, logger) {
        if (!enforceCacheDelay) {
            logger.trace('[minimumDelay] no minimum delay enforced due to enforceCacheDelay being false');
            return 0;
        }
        const cacheDelay = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsCacheDelay, this._expService);
        const rebasedCacheDelay = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsRebasedCacheDelay, this._expService);
        const subsequentCacheDelay = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsSubsequentCacheDelay, this._expService);
        const speculativeRequestDelay = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsSpeculativeRequestDelay, this._expService);
        let minimumResponseDelay = cacheDelay;
        if (isRebasedCachedEdit && rebasedCacheDelay !== undefined) {
            minimumResponseDelay = rebasedCacheDelay;
        }
        else if (isSubsequentCachedEdit && subsequentCacheDelay !== undefined) {
            minimumResponseDelay = subsequentCacheDelay;
        }
        else if (isFromSpeculativeRequest && speculativeRequestDelay !== undefined) {
            minimumResponseDelay = speculativeRequestDelay;
        }
        const nextEditProviderCallLatency = Date.now() - triggerTime;
        // if the provider call took longer than the minimum delay, we don't need to delay further
        const delay = Math.max(0, minimumResponseDelay - nextEditProviderCallLatency);
        logger.trace(`[minimumDelay] expected delay: ${minimumResponseDelay}ms, effective delay: ${delay}. isRebasedCachedEdit: ${isRebasedCachedEdit} (rebasedCacheDelay: ${rebasedCacheDelay}), isSubsequentCachedEdit: ${isSubsequentCachedEdit} (subsequentCacheDelay: ${subsequentCacheDelay}), isFromSpeculativeRequest: ${isFromSpeculativeRequest} (speculativeRequestDelay: ${speculativeRequestDelay})`);
        return delay;
    }
    handleShown(suggestion) {
        this._lastShownTime = Date.now();
        this._lastShownSuggestionId = suggestion.requestId;
        this._lastOutcome = undefined; // clear so that outcome is "pending" until resolved
        this._specManager.clearScheduled(); // clear any previously scheduled speculative
        // Trigger speculative request for the post-edit document state
        const speculativeRequestsEnablement = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsSpeculativeRequests, this._expService);
        if (speculativeRequestsEnablement === xtabPromptOptions_1.SpeculativeRequestsEnablement.On) {
            // If the originating stream is still running, defer the speculative request
            // until the stream completes. If more edits come from this stream, the
            // schedule is cleared (the shown edit wasn't the last one). The speculative
            // request only fires when the stream ends with the shown edit as the last one.
            const originatingRequest = this._pendingStatelessNextEditRequest;
            if (originatingRequest && originatingRequest.headerRequestId === suggestion.source.headerRequestId) {
                this._specManager.schedule({
                    suggestion,
                    headerRequestId: originatingRequest.headerRequestId,
                });
            }
            else {
                void this._triggerSpeculativeRequest(suggestion);
            }
        }
    }
    async _triggerSpeculativeRequest(suggestion) {
        const result = suggestion.result;
        if (!result?.edit) {
            return;
        }
        const docId = result.targetDocumentId;
        if (!docId) {
            return;
        }
        const logContext = new inlineEditLogContext_1.InlineEditRequestLogContext(docId.uri, 0, undefined);
        const sw = new stopwatch_1.StopWatch();
        const logger = this._logger.createSubLogger('_triggerSpeculativeRequest')
            .withExtraTarget(logService_1.LogTarget.fromCallback((_level, msg) => {
            logContext.trace(`[${Math.floor(sw.elapsed()).toString().padStart(4, ' ')}ms] ${msg}`);
        }));
        // Compute the post-edit document content
        const postEditContent = result.edit.replace(result.documentBeforeEdits.value);
        const preciseEdit = result.edit.removeCommonSuffixPrefix(result.documentBeforeEdits.value);
        const postEditCursorOffset = preciseEdit.replaceRange.start + preciseEdit.newText.length;
        const postEditCursorOffsetRange = new offsetRange_1.OffsetRange(postEditCursorOffset, postEditCursorOffset);
        const selections = [postEditCursorOffsetRange];
        const rootedEdit = new edit_1.RootedEdit(result.documentBeforeEdits, new stringEdit_1.StringEdit([result.edit]));
        const postEditContentST = new abstractText_1.StringText(postEditContent);
        let cachedEdit = this._nextEditCache.lookupNextEdit(docId, postEditContentST, selections);
        let shiftedSelection = postEditCursorOffsetRange;
        if (cachedEdit) {
            // first cachedEdit should be without edits because of noSuggestions caching
            if (cachedEdit.edit) {
                logger.trace('already have cached edit for post-edit state');
                return;
            }
            else if (cachedEdit.editWindow) {
                logger.trace('have cached no-suggestions entry for post-edit state, but it has an edit window. Checking if shifting selection based on cursor placement config can yield a cached edit');
                const cursorPlacement = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsSpeculativeRequestsCursorPlacement, this._expService);
                if (cursorPlacement === xtabPromptOptions_1.SpeculativeRequestsCursorPlacement.AfterEditWindow) {
                    logger.trace('cursor placement config is AfterEditWindow, shifting selection to after edit window');
                    shiftedSelection = NextEditProvider_1.shiftSelectionAfterEditWindow(postEditContentST, cachedEdit.editWindow);
                    cachedEdit = this._nextEditCache.lookupNextEdit(docId, postEditContentST, [shiftedSelection]);
                    if (cachedEdit?.edit) {
                        logger.trace('already have cached edit for post-edit state (after shifting selection)');
                        return;
                    }
                    else {
                        logger.trace('no cached edit even after shifting selection');
                    }
                }
                else {
                    logger.trace(`cursor placement config is ${cursorPlacement}, not shifting selection`);
                }
            }
            else {
                logger.trace('already have cached no-suggestions entry for post-edit state');
                return;
            }
        }
        // Check if we already have a pending request for the post-edit state
        if (this._pendingStatelessNextEditRequest?.documentBeforeEdits.value === postEditContent) {
            logger.trace('already have pending request for post-edit state');
            return;
        }
        // Check if we already have a speculative request for this post-edit state
        const existingSpec = this._specManager.pending;
        if (existingSpec?.docId === docId && existingSpec?.postEditContent === postEditContent) {
            logger.trace('already have speculative request for post-edit state');
            return;
        }
        // Get the document to trigger speculative fetch
        // Note: targetDocumentId is defined when the suggestion targets a different document
        // Otherwise, use the file path from the log context
        const doc = this._workspace.getDocument(docId);
        if (!doc) {
            logger.trace('document not found for speculative request');
            return;
        }
        // Note: any previous speculative request will be cancelled (as `Replaced`)
        // by `_specManager.setPending` once the new request is actually installed —
        // see the `setPending` call at the end of this method. We deliberately do
        // not cancel earlier so the prior speculative stays available for reuse
        // while the new one is being constructed.
        const historyContext = this._historyContextProvider.getHistoryContext(docId);
        if (!historyContext) {
            logger.trace('no history context for speculative request');
            return;
        }
        const req = new NextEditFetchRequest(`sp-${suggestion.source.opportunityId}`, logContext, undefined, true, `sp-${(0, uuid_1.generateUuid)()}`);
        logger.trace(`triggering speculative request for post-edit state (opportunityId=${req.opportunityId}, headerRequestId=${req.headerRequestId})`);
        try {
            const speculativeRequest = await this._createSpeculativeRequest(req, doc, shiftedSelection, historyContext, postEditContent, rootedEdit, result.edit, {
                triggeredBySpeculativeRequest: suggestion.source.isSpeculative,
                isSubsequentEdit: suggestion.result?.isSubsequentEdit ?? false,
            }, logger);
            if (speculativeRequest) {
                // Capture trajectory data: while the user is typing in `docId`, the
                // document is on a "type-through" trajectory iff:
                //   doc = preEdit[0..editStart] + newText[0..k] + preEdit[editEnd..]
                // for some 0 <= k <= newText.length. Storing the prefix/suffix/newText
                // (already-CRLF-normalized via `result.edit.newText` whose newlines
                // match the original document) lets us check this in O(|cur|) on doc changes.
                const preEditValue = result.documentBeforeEdits.value;
                const trajectoryPrefix = preEditValue.slice(0, preciseEdit.replaceRange.start);
                const trajectorySuffix = preEditValue.slice(preciseEdit.replaceRange.endExclusive);
                const trajectoryNewText = preciseEdit.newText;
                this._specManager.setPending({
                    request: speculativeRequest,
                    docId,
                    postEditContent,
                    trajectoryPrefix,
                    trajectorySuffix,
                    trajectoryNewText,
                });
            }
        }
        catch (e) {
            logger.trace(`speculative request failed: ${errors_1.ErrorUtils.toString(e)}`);
        }
    }
    /**
     * Creates and starts a speculative request for the post-edit document state.
     * The request will populate the cache so that when the user accepts the suggestion,
     * the next NES request can reuse or find the result in cache.
     */
    async _createSpeculativeRequest(req, doc, shiftedSelection, historyContext, postEditContent, rootedEdit, appliedEdit, { triggeredBySpeculativeRequest, isSubsequentEdit }, parentLogger) {
        const curDocId = doc.id;
        const recording = this._debugRecorder?.getRecentLog();
        const logContext = req.log;
        logContext.setStatelessNextEditProviderId(this._statelessNextEditProvider.ID);
        const logger = parentLogger.createSubLogger('_createSpeculativeRequest');
        const activeDocAndIdx = historyContext.getDocumentAndIdx(curDocId);
        if (!activeDocAndIdx) {
            logger.trace('active doc not found in history context');
            return undefined;
        }
        // Create the post-edit document content
        const postEditText = new abstractText_1.StringText(postEditContent);
        // Process documents, but for the active document, use the post-edit state
        const projectedDocuments = historyContext.documents.map(docHist => {
            if (docHist.docId !== curDocId) {
                return this._processDoc(docHist);
            }
            else {
                // For the active document, create a version representing post-edit state
                // The "recent edit" from the model's perspective is the NES edit we just applied
                const workspaceRoot = this._workspace.getWorkspaceRoot(curDocId);
                const postEditEdit = new stringEdit_1.StringEdit([appliedEdit]);
                const postEditLineEdit = rootedLineEdit_1.RootedLineEdit.fromEdit(new edit_1.RootedEdit(doc.value.get(), postEditEdit)).removeCommonSuffixPrefixLines().edit;
                const nextEditDoc = new statelessNextEditProvider_1.StatelessNextEditDocument(curDocId, workspaceRoot, docHist.languageId, doc.value.get().getLines(), // lines before the NES edit
                postEditLineEdit, // the NES edit as LineEdit
                doc.value.get(), // document before NES edit
                edit_1.Edits.single(postEditEdit), // the NES edit as Edits
                shiftedSelection);
                return {
                    recentEdit: new edit_1.RootedEdit(doc.value.get(), postEditEdit),
                    nextEditDoc,
                    documentAfterEdits: postEditText,
                };
            }
        });
        const xtabEditHistory = this._xtabHistoryTracker.getHistory();
        const suggestedEdit = { kind: 'edit', docId: curDocId, edit: rootedEdit };
        xtabEditHistory.push(suggestedEdit);
        const firstEdit = new async_1.DeferredPromise();
        // FIXME@ulugbekna: implement advanced expansion
        const autoExpandEditWindowLinesSetting = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsSpeculativeRequestsAutoExpandEditWindowLines, this._expService);
        let nLinesEditWindow;
        switch (autoExpandEditWindowLinesSetting) {
            case xtabPromptOptions_1.SpeculativeRequestsAutoExpandEditWindowLines.Off:
                nLinesEditWindow = undefined;
                break;
            case xtabPromptOptions_1.SpeculativeRequestsAutoExpandEditWindowLines.Always:
                nLinesEditWindow = this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsAutoExpandEditWindowLines, this._expService);
                break;
            case xtabPromptOptions_1.SpeculativeRequestsAutoExpandEditWindowLines.Smart: {
                const isModelOnRightTrack = triggeredBySpeculativeRequest || isSubsequentEdit;
                nLinesEditWindow = (isModelOnRightTrack
                    ? this._configService.getExperimentBasedConfig(configurationService_1.ConfigKey.TeamInternal.InlineEditsAutoExpandEditWindowLines, this._expService)
                    : undefined);
                break;
            }
            default:
                (0, assert_1.assertNever)(autoExpandEditWindowLinesSetting);
        }
        const nextEditRequest = new statelessNextEditProvider_1.StatelessNextEditRequest(req.headerRequestId, req.opportunityId, postEditText, // documentBeforeEdits is the post-edit state
        projectedDocuments.map(d => d.nextEditDoc), activeDocAndIdx.idx, xtabEditHistory, firstEdit, nLinesEditWindow, true, // isSpeculative
        logContext, undefined, // recordingBookmark
        recording, undefined);
        logContext.setRequestInput(nextEditRequest);
        logger.trace('starting speculative provider call');
        // Start the provider call - this runs in the background and populates the cache
        const label = `NES | spec | ${(0, path_1.basename)(doc.id.toUri().fsPath)} (v${doc.version.get()})`;
        const capturingToken = new capturingToken_1.CapturingToken(label, undefined);
        void this._requestLogger.captureInvocation(capturingToken, async () => {
            this._addLiveLogContextEntry(logContext, label);
            try {
                await this._runSpeculativeProviderCall(nextEditRequest, projectedDocuments, curDocId, req, shiftedSelection.start, logger);
            }
            catch (e) {
                logContext.setError(e);
            }
            finally {
                logContext.markCompleted();
            }
        });
        return nextEditRequest;
    }
    /**
     * Runs the provider call for a speculative request and caches results.
     */
    async _runSpeculativeProviderCall(nextEditRequest, projectedDocuments, curDocId, req, cursorOffset, parentLogger) {
        const logger = parentLogger.createSubLogger('_runSpeculativeProviderCall');
        const xtabEditHistory = nextEditRequest.xtabEditHistory;
        const statePerDoc = createDocStateLookupMap(projectedDocuments, xtabEditHistory);
        const logContext = req.log;
        const editStream = this._statelessNextEditProvider.provideNextEdit(nextEditRequest, logger, logContext, nextEditRequest.cancellationTokenSource.token);
        let ithEdit = -1;
        try {
            let res = await editStream.next();
            if (res.done) {
                nextEditRequest.firstEdit.complete(result_1.Result.error(res.value.v));
                nextEditRequest.setResult(new statelessNextEditProvider_1.StatelessNextEditResult(result_1.Result.error(res.value.v), res.value.telemetryBuilder));
                logContext.markAsNoSuggestions();
                logger.trace('speculative request completed with no edits');
            }
            else {
                (async () => {
                    while (!res.done) {
                        ++ithEdit;
                        const streamedEdit = res.value.v;
                        const targetDocState = statePerDoc.get(streamedEdit.targetDocument ?? curDocId);
                        const singleLineEdit = streamedEdit.edit;
                        const lineEdit = new lineEdit_1.LineEdit([singleLineEdit]);
                        const edit = convertLineEditToEdit(lineEdit, targetDocState.baseDocState);
                        const rebasedEdit = edit.tryRebase(targetDocState.editsSoFar);
                        if (rebasedEdit === undefined) {
                            logger.trace(`speculative edit ${ithEdit} rebasing failed`);
                            res = await editStream.next();
                            continue;
                        }
                        targetDocState.editsSoFar = targetDocState.editsSoFar.compose(rebasedEdit);
                        if (rebasedEdit.replacements.length === 1) {
                            const nextEditReplacement = rebasedEdit.replacements[0];
                            targetDocState.nextEdits.push(nextEditReplacement);
                            // Populate the cache with the speculative result
                            const cachedEdit = this._nextEditCache.setKthNextEdit(targetDocState.docId, targetDocState.docContents, ithEdit === 0 ? streamedEdit.window : undefined, nextEditReplacement, ithEdit, ithEdit === 0 ? targetDocState.nextEdits : undefined, undefined, // no userEditSince for speculative
                            req, { isFromCursorJump: streamedEdit.isFromCursorJump, originalEditWindow: streamedEdit.originalWindow, cursorOffset: targetDocState.docId === curDocId ? cursorOffset : undefined });
                            if (!nextEditRequest.firstEdit.isSettled && cachedEdit) {
                                nextEditRequest.firstEdit.complete(result_1.Result.ok(cachedEdit));
                                nextEditRequest.setResult(new statelessNextEditProvider_1.StatelessNextEditResult(result_1.Result.ok(undefined), res.value.telemetryBuilder));
                                logContext.setResponseResults([nextEditReplacement]);
                            }
                            logger.trace(`cached speculative edit ${ithEdit}`);
                        }
                        targetDocState.docContents = rebasedEdit.applyOnText(targetDocState.docContents);
                        res = await editStream.next();
                    }
                })().finally(() => {
                    if (!nextEditRequest.firstEdit.isSettled) {
                        nextEditRequest.firstEdit.complete(result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.Uncategorized(new Error('Speculative request ended without edits'))));
                        nextEditRequest.setResult(new statelessNextEditProvider_1.StatelessNextEditResult(result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.Uncategorized(new Error('Speculative request ended without edits'))), res.value.telemetryBuilder));
                        logContext.markAsNoSuggestions();
                    }
                });
            }
            logger.trace(`speculative request completed with ${ithEdit + 1} edits`);
        }
        catch (e) {
            logger.trace(`speculative provider call error: ${errors_1.ErrorUtils.toString(e)}`);
        }
    }
    static shiftSelectionAfterEditWindow(postEditContentST, editWindowOffsetRange) {
        const trans = postEditContentST.getTransformer();
        const endOfEditWindow = trans.getPosition(editWindowOffsetRange.endExclusive - 1);
        const shiftedCursorLineNumber = (endOfEditWindow.lineNumber + 1 < postEditContentST.lineRange.endLineNumberExclusive
            ? endOfEditWindow.lineNumber + 1
            : endOfEditWindow.lineNumber);
        const shiftedSelectionCursorOffset = trans.getOffset(new position_1.Position(shiftedCursorLineNumber, 1));
        const shiftedSelection = new offsetRange_1.OffsetRange(shiftedSelectionCursorOffset, shiftedSelectionCursorOffset);
        return shiftedSelection;
    }
    handleAcceptance(docId, suggestion) {
        this.runSnippy(docId, suggestion);
        this._statelessNextEditProvider.handleAcceptance?.();
        this._lastOutcome = NesOutcome.Accepted;
        const logger = this._logger.createSubLogger(suggestion.source.opportunityId.substring(4, 8)).createSubLogger('handleAcceptance');
        if (suggestion === this._lastNextEditResult) {
            logger.trace('setting shouldExpandEditWindow to true due to acceptance of last suggestion');
            this._shouldExpandEditWindow = true;
        }
        else {
            logger.trace('NOT setting shouldExpandEditWindow to true because suggestion is not the last suggestion');
        }
    }
    handleRejection(docId, suggestion) {
        (0, types_1.assertType)(suggestion.result, '@ulugbekna: undefined edit cannot be rejected?');
        // The user rejected the suggestion, so the speculative request (which
        // predicted the post-accept state) will never be reused. Cancel it to
        // avoid wasting a server slot.
        this._specManager.cancelAll("rejected" /* SpeculativeCancelReason.Rejected */);
        const shownDuration = Date.now() - this._lastShownTime;
        if (shownDuration > 1000 && suggestion.result.edit) {
            // we can argue that the user had the time to review this
            // so it wasn't an accidental rejection
            this._rejectionCollector.reject(docId, suggestion.result.edit);
            this._nextEditCache.rejectedNextEdit(suggestion.source.headerRequestId);
        }
        this._lastRejectionTime = Date.now();
        this._lastOutcome = NesOutcome.Rejected;
        this._statelessNextEditProvider.handleRejection?.();
    }
    handleIgnored(docId, suggestion, supersededBy) {
        this._lastOutcome = NesOutcome.Ignored;
        // Check if this was the last shown suggestion
        const wasShown = this._lastShownSuggestionId === suggestion.requestId;
        const wasSuperseded = supersededBy !== undefined;
        if (wasShown && !wasSuperseded) {
            // The shown suggestion was dismissed (not superseded by a new one),
            // so the speculative request for its post-accept state is useless.
            this._specManager.cancelAll("ignoredDismissed" /* SpeculativeCancelReason.IgnoredDismissed */);
            this._statelessNextEditProvider.handleIgnored?.();
        }
        // Note: the superseded case is intentionally NOT handled here. The trajectory
        // check on `_specManager.onActiveDocumentChanged` already cancels the
        // speculative iff the user's edit moved off the type-through trajectory; if
        // the new (superseding) suggestion is just a continuation of the old one
        // (e.g. typed `i` while `ibonacci` was shown → now `bonacci` is shown), the
        // speculative's `postEditContent` is still the right bet and we keep it.
    }
    async runSnippy(docId, suggestion) {
        if (suggestion.result === undefined || suggestion.result.edit === undefined) {
            return;
        }
        this._snippyService.handlePostInsertion(docId.toUri(), suggestion.result.documentBeforeEdits, suggestion.result.edit);
    }
    _addLiveLogContextEntry(logContext, debugNameOverride) {
        this._requestLogger.addEntry({
            type: "MarkdownContentRequest" /* LoggedRequestKind.MarkdownContentRequest */,
            debugName: debugNameOverride ?? logContext.getDebugName(),
            icon: () => logContext.getIcon(),
            startTimeMs: logContext.time,
            markdownContent: () => logContext.toLogDocument(),
            onDidChange: logContext.onDidChange,
            isVisible: () => logContext.includeInLogTree,
        });
    }
    clearCache() {
        this._nextEditCache.clear();
        this._rejectionCollector.clear();
        // Any in-flight speculative would land its result into a cache that's
        // meant to be empty (and may be based on a now-stale model/auth/prompt).
        this._specManager.cancelAll("cacheCleared" /* SpeculativeCancelReason.CacheCleared */);
    }
};
exports.NextEditProvider = NextEditProvider;
exports.NextEditProvider = NextEditProvider = NextEditProvider_1 = __decorate([
    __param(5, configurationService_1.IConfigurationService),
    __param(6, snippyService_1.ISnippyService),
    __param(7, logService_1.ILogService),
    __param(8, nullExperimentationService_1.IExperimentationService),
    __param(9, requestLogger_1.IRequestLogger)
], NextEditProvider);
function assertDefined(value) {
    if (!value) {
        throw new errors_2.BugIndicatingError('expected value to be defined, but it was not');
    }
    return value;
}
class NextEditFetchRequest {
    constructor(opportunityId, log, providerRequestStartDateTime, isSpeculative, headerRequestId = (0, uuid_1.generateUuid)()) {
        this.opportunityId = opportunityId;
        this.log = log;
        this.providerRequestStartDateTime = providerRequestStartDateTime;
        this.isSpeculative = isSpeculative;
        this.headerRequestId = headerRequestId;
    }
}
exports.NextEditFetchRequest = NextEditFetchRequest;
//# sourceMappingURL=nextEditProvider.js.map