"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RenameProviderImpl = void 0;
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../../lib/documents");
const utils_1 = require("../../../utils");
const DocumentSnapshot_1 = require("../DocumentSnapshot");
const utils_2 = require("../utils");
const typescript_1 = __importDefault(require("typescript"));
const utils_3 = require("./utils");
const svelte_ast_utils_1 = require("../svelte-ast-utils");
class RenameProviderImpl {
    constructor(lsAndTsDocResolver, configManager) {
        this.lsAndTsDocResolver = lsAndTsDocResolver;
        this.configManager = configManager;
    }
    // TODO props written as `export {x as y}` are not supported yet.
    async prepareRename(document, position) {
        const { lang, tsDoc } = await this.getLSAndTSDoc(document);
        const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
        const renameInfo = this.getRenameInfo(lang, tsDoc, document, position, offset);
        if (!renameInfo) {
            return null;
        }
        return this.mapRangeToOriginal(tsDoc, renameInfo.triggerSpan);
    }
    async rename(document, position, newName) {
        const { lang, tsDoc } = await this.getLSAndTSDoc(document);
        const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
        const renameInfo = this.getRenameInfo(lang, tsDoc, document, position, offset);
        if (!renameInfo) {
            return null;
        }
        const renameLocations = lang.findRenameLocations(tsDoc.filePath, offset, false, false, true);
        if (!renameLocations) {
            return null;
        }
        const docs = new utils_3.SnapshotMap(this.lsAndTsDocResolver);
        docs.set(tsDoc.filePath, tsDoc);
        let convertedRenameLocations = await this.mapAndFilterRenameLocations(renameLocations, docs, renameInfo.isStore ? `$${newName}` : undefined);
        convertedRenameLocations.push(...(await this.enhanceRenamesInCaseOf$Store(renameLocations, newName, docs, lang)));
        convertedRenameLocations = this.checkShortHandBindingOrSlotLetLocation(lang, convertedRenameLocations, docs);
        const additionalRenameForPropRenameInsideComponentWithProp = await this.getAdditionLocationsForRenameOfPropInsideComponentWithProp(document, tsDoc, position, convertedRenameLocations, docs, lang, newName);
        const additionalRenamesForPropRenameOutsideComponentWithProp = 
        // This is an either-or-situation, don't do both
        additionalRenameForPropRenameInsideComponentWithProp.length > 0
            ? []
            : await this.getAdditionalLocationsForRenameOfPropInsideOtherComponent(convertedRenameLocations, docs, lang, tsDoc.filePath);
        convertedRenameLocations = [
            ...convertedRenameLocations,
            ...additionalRenameForPropRenameInsideComponentWithProp,
            ...additionalRenamesForPropRenameOutsideComponentWithProp
        ];
        return (0, utils_1.unique)(convertedRenameLocations.filter((loc) => loc.range.start.line >= 0 && loc.range.end.line >= 0)).reduce((acc, loc) => {
            const uri = (0, utils_1.pathToUrl)(loc.fileName);
            if (!acc.changes[uri]) {
                acc.changes[uri] = [];
            }
            acc.changes[uri].push({
                newText: (loc.prefixText || '') + (loc.newName || newName) + (loc.suffixText || ''),
                range: loc.range
            });
            return acc;
        }, { changes: {} });
    }
    getRenameInfo(lang, tsDoc, doc, originalPosition, generatedOffset) {
        var _a, _b, _c;
        // Don't allow renames in error-state, because then there is no generated svelte2tsx-code
        // and rename cannot work
        if (tsDoc.parserError) {
            return null;
        }
        const renameInfo = lang.getRenameInfo(tsDoc.filePath, generatedOffset, {
            allowRenameOfImportPath: false
        });
        if (!renameInfo.canRename ||
            ((_a = renameInfo.fullDisplayName) === null || _a === void 0 ? void 0 : _a.includes('JSX.IntrinsicElements')) ||
            (renameInfo.kind === typescript_1.default.ScriptElementKind.jsxAttribute &&
                !(0, utils_3.isComponentAtPosition)(doc, tsDoc, originalPosition))) {
            return null;
        }
        const svelteNode = tsDoc.svelteNodeAt(originalPosition);
        if ((0, documents_1.isInHTMLTagRange)(doc.html, doc.offsetAt(originalPosition)) ||
            (0, svelte_ast_utils_1.isAttributeName)(svelteNode, 'Element') ||
            (0, svelte_ast_utils_1.isEventHandler)(svelteNode, 'Element')) {
            return null;
        }
        // If $store is renamed, only allow rename for $|store|
        if (tsDoc.getFullText().charAt(renameInfo.triggerSpan.start) === '$') {
            const definition = (_c = (_b = lang.getDefinitionAndBoundSpan(tsDoc.filePath, generatedOffset)) === null || _b === void 0 ? void 0 : _b.definitions) === null || _c === void 0 ? void 0 : _c[0];
            if (definition && (0, utils_3.isTextSpanInGeneratedCode)(tsDoc.getFullText(), definition.textSpan)) {
                renameInfo.triggerSpan.start++;
                renameInfo.triggerSpan.length--;
                renameInfo.isStore = true;
            }
        }
        return renameInfo;
    }
    /**
     * If the user renames a store variable, we need to rename the corresponding $store variables
     * and vice versa.
     */
    async enhanceRenamesInCaseOf$Store(renameLocations, newName, docs, lang) {
        for (const loc of renameLocations) {
            const snapshot = await docs.retrieve(loc.fileName);
            if ((0, utils_3.isTextSpanInGeneratedCode)(snapshot.getFullText(), loc.textSpan)) {
                if ((0, utils_3.isStoreVariableIn$storeDeclaration)(snapshot.getFullText(), loc.textSpan.start)) {
                    // User renamed store, also rename corresponding $store locations
                    const storeRenameLocations = lang.findRenameLocations(snapshot.filePath, (0, utils_3.get$storeOffsetOf$storeDeclaration)(snapshot.getFullText(), loc.textSpan.start), false, false, true);
                    return await this.mapAndFilterRenameLocations(storeRenameLocations, docs, `$${newName}`);
                }
                else if ((0, utils_3.is$storeVariableIn$storeDeclaration)(snapshot.getFullText(), loc.textSpan.start)) {
                    // User renamed $store, also rename corresponding store
                    const storeRenameLocations = lang.findRenameLocations(snapshot.filePath, (0, utils_3.getStoreOffsetOf$storeDeclaration)(snapshot.getFullText(), loc.textSpan.start), false, false, true);
                    return await this.mapAndFilterRenameLocations(storeRenameLocations, docs);
                    // TODO once we allow providePrefixAndSuffixTextForRename to be configurable,
                    // we need to add one more step to update all other $store usages in other files
                }
            }
        }
        return [];
    }
    /**
     * If user renames prop of component A inside component A,
     * we need to handle the rename of the prop of A ourselves.
     * Reason: the rename will do {oldPropName: newPropName}, meaning
     * the rename will not propagate further, so we have to handle
     * the conversion to {newPropName: newPropName} ourselves.
     */
    async getAdditionLocationsForRenameOfPropInsideComponentWithProp(document, tsDoc, position, convertedRenameLocations, snapshots, lang, newName) {
        // First find out if it's really the "rename prop inside component with that prop" case
        // Use original document for that because only there the `export` is present.
        // ':' for typescript's type operator (`export let bla: boolean`)
        // '//' and '/*' for comments (`export let bla// comment` or `export let bla/* comment */`)
        const regex = new RegExp(`export\\s+let\\s+${this.getVariableAtPosition(tsDoc, lang, position)}($|\\s|;|:|\/\*|\/\/)`);
        const isRenameInsideComponentWithProp = regex.test((0, documents_1.getLineAtPosition)(position, document.getText()));
        if (!isRenameInsideComponentWithProp) {
            return [];
        }
        // We now know that the rename happens at `export let X` -> let's find the corresponding
        // prop rename further below in the document.
        const updatePropLocation = this.findLocationWhichWantsToUpdatePropName(convertedRenameLocations, snapshots);
        if (!updatePropLocation) {
            return [];
        }
        // Typescript does a rename of `oldPropName: newPropName` -> find oldPropName and rename that, too.
        const idxOfOldPropName = tsDoc
            .getFullText()
            .lastIndexOf(':', updatePropLocation.textSpan.start);
        // This requires svelte2tsx to have the properties written down like `return props: {bla: bla}`.
        // It would not work for `return props: {bla}` because then typescript would do a rename of `{bla: renamed}`,
        // so other locations would not be affected.
        const replacementsForProp = (lang.findRenameLocations(updatePropLocation.fileName, idxOfOldPropName, false, false, true) || []).filter((rename) => 
        // filter out all renames inside the component except the prop rename,
        // because the others were done before and then would show up twice, making a wrong rename.
        rename.fileName !== updatePropLocation.fileName ||
            this.isInSvelte2TsxPropLine(tsDoc, rename));
        const renameLocations = await this.mapAndFilterRenameLocations(replacementsForProp, snapshots);
        const bind = 'bind:';
        // Adjust shorthands
        return renameLocations.map((location) => {
            var _a, _b;
            if (updatePropLocation.fileName === location.fileName) {
                return location;
            }
            const sourceFile = (_a = lang.getProgram()) === null || _a === void 0 ? void 0 : _a.getSourceFile(location.fileName);
            if (!sourceFile ||
                location.fileName !== sourceFile.fileName ||
                location.range.start.line < 0 ||
                location.range.end.line < 0) {
                return location;
            }
            const snapshot = snapshots.get(location.fileName);
            if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
                return location;
            }
            const { parent } = snapshot;
            let rangeStart = parent.offsetAt(location.range.start);
            let suffixText = (_b = location.suffixText) === null || _b === void 0 ? void 0 : _b.trimStart();
            // suffix is of the form `: oldVarName` -> hints at a shorthand
            if (!(suffixText === null || suffixText === void 0 ? void 0 : suffixText.startsWith(':')) || !(0, documents_1.getNodeIfIsInStartTag)(parent.html, rangeStart)) {
                return location;
            }
            const original = parent.getText({
                start: vscode_languageserver_1.Position.create(location.range.start.line, location.range.start.character - bind.length),
                end: location.range.end
            });
            if (original.startsWith(bind)) {
                // bind:|foo| -> bind:|newName|={foo}
                return {
                    ...location,
                    prefixText: '',
                    suffixText: `={${original.slice(bind.length)}}`
                };
            }
            if (snapshot.getOriginalText().charAt(rangeStart - 1) === '{') {
                // {|foo|} -> |{foo|}
                rangeStart--;
                return {
                    ...location,
                    range: {
                        start: parent.positionAt(rangeStart),
                        end: location.range.end
                    },
                    // |{foo|} -> newName=|{foo|}
                    newName: parent.getText(location.range),
                    prefixText: `${newName}={`,
                    suffixText: ''
                };
            }
            return location;
        });
    }
    /**
     * If user renames prop of component A inside component B,
     * we need to handle the rename of the prop of A ourselves.
     * Reason: the rename will rename the prop in the computed svelte2tsx code,
     * but not the `export let X` code in the original because the
     * rename does not propagate further than the prop.
     * This additional logic/propagation is done in this method.
     */
    async getAdditionalLocationsForRenameOfPropInsideOtherComponent(convertedRenameLocations, snapshots, lang, requestedFileName) {
        // Check if it's a prop rename
        const updatePropLocation = this.findLocationWhichWantsToUpdatePropName(convertedRenameLocations, snapshots);
        if (!updatePropLocation) {
            return [];
        }
        const getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(typescript_1.default.sys.useCaseSensitiveFileNames);
        if (getCanonicalFileName(updatePropLocation.fileName) ===
            getCanonicalFileName(requestedFileName)) {
            return [];
        }
        // Find generated `export let`
        const doc = snapshots.get(updatePropLocation.fileName);
        const match = this.matchGeneratedExportLet(doc, updatePropLocation);
        if (!match) {
            return [];
        }
        // Use match to replace that let, too.
        const idx = (match.index || 0) + match[0].lastIndexOf(match[1]);
        const replacementsForProp = lang.findRenameLocations(updatePropLocation.fileName, idx, false, false) || [];
        return this.checkShortHandBindingOrSlotLetLocation(lang, await this.mapAndFilterRenameLocations(replacementsForProp, snapshots), snapshots);
    }
    // --------> svelte2tsx?
    matchGeneratedExportLet(snapshot, updatePropLocation) {
        const regex = new RegExp(
        // no 'export let', only 'let', because that's what it's translated to in svelte2tsx
        // '//' and '/*' for comments (`let bla/*Ωignore_startΩ*/`)
        `\\s+let\\s+(${snapshot
            .getFullText()
            .substring(updatePropLocation.textSpan.start, updatePropLocation.textSpan.start + updatePropLocation.textSpan.length)})($|\\s|;|:|\/\*|\/\/)`);
        const match = snapshot.getFullText().match(regex);
        return match;
    }
    findLocationWhichWantsToUpdatePropName(convertedRenameLocations, snapshots) {
        return convertedRenameLocations.find((loc) => {
            // Props are not in mapped range
            if (loc.range.start.line >= 0 && loc.range.end.line >= 0) {
                return;
            }
            const snapshot = snapshots.get(loc.fileName);
            // Props are in svelte snapshots only
            if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
                return false;
            }
            return this.isInSvelte2TsxPropLine(snapshot, loc);
        });
    }
    // --------> svelte2tsx?
    isInSvelte2TsxPropLine(snapshot, loc) {
        return (0, utils_3.isAfterSvelte2TsxPropsReturn)(snapshot.getFullText(), loc.textSpan.start);
    }
    /**
     * The rename locations the ts language services hands back are relative to the
     * svelte2tsx generated code -> map it back to the original document positions.
     * Some of those positions could be unmapped (line=-1), these are handled elsewhere.
     * Also filter out wrong renames.
     */
    async mapAndFilterRenameLocations(renameLocations, snapshots, newName) {
        const mappedLocations = await Promise.all(renameLocations.map(async (loc) => {
            const snapshot = await snapshots.retrieve(loc.fileName);
            if (!(0, utils_3.isTextSpanInGeneratedCode)(snapshot.getFullText(), loc.textSpan)) {
                return {
                    ...loc,
                    range: this.mapRangeToOriginal(snapshot, loc.textSpan),
                    newName
                };
            }
        }));
        return this.filterWrongRenameLocations(mappedLocations.filter(utils_1.isNotNullOrUndefined));
    }
    filterWrongRenameLocations(mappedLocations) {
        return (0, utils_1.filterAsync)(mappedLocations, async (loc) => {
            const snapshot = await this.getSnapshot(loc.fileName);
            if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
                return true;
            }
            const content = snapshot.getText(0, snapshot.getLength());
            // When the user renames a Svelte component, ts will also want to rename
            // `__sveltets_2_instanceOf(TheComponentToRename)` or
            // `__sveltets_1_ensureType(TheComponentToRename,..`. Prevent that.
            // Additionally, we cannot rename the hidden variable containing the store value
            return (notPrecededBy('__sveltets_2_instanceOf(') &&
                notPrecededBy('__sveltets_1_ensureType(') && // no longer necessary for new transformation
                notPrecededBy('= __sveltets_2_store_get('));
            function notPrecededBy(str) {
                return (content.lastIndexOf(str, loc.textSpan.start) !== loc.textSpan.start - str.length);
            }
        });
    }
    mapRangeToOriginal(snapshot, textSpan) {
        // We need to work around a current svelte2tsx limitation: Replacements and
        // source mapping is done in such a way that sometimes the end of the range is unmapped
        // and the index of the last character is returned instead (which is one less).
        // Most of the time this is not much of a problem, but in the context of renaming, it is.
        // We work around that by adding +1 to the end, if necessary.
        // This can be done because
        // 1. we know renames can only ever occur in one line
        // 2. the generated svelte2tsx code will not modify variable names, so we know
        //    the original range should be the same length as the textSpan's length
        const range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, textSpan));
        if (range.end.character - range.start.character < textSpan.length) {
            range.end.character++;
        }
        return range;
    }
    getVariableAtPosition(tsDoc, lang, position) {
        const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
        const { start, length } = lang.getSmartSelectionRange(tsDoc.filePath, offset).textSpan;
        return tsDoc.getText(start, start + length);
    }
    async getLSAndTSDoc(document) {
        return this.lsAndTsDocResolver.getLSAndTSDoc(document);
    }
    getSnapshot(filePath) {
        return this.lsAndTsDocResolver.getSnapshot(filePath);
    }
    checkShortHandBindingOrSlotLetLocation(lang, renameLocations, snapshots) {
        const bind = 'bind:';
        return renameLocations.map((location) => {
            var _a, _b;
            const sourceFile = (_a = lang.getProgram()) === null || _a === void 0 ? void 0 : _a.getSourceFile(location.fileName);
            if (!sourceFile ||
                location.fileName !== sourceFile.fileName ||
                location.range.start.line < 0 ||
                location.range.end.line < 0) {
                return location;
            }
            const snapshot = snapshots.get(location.fileName);
            if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
                return location;
            }
            const { parent } = snapshot;
            let rangeStart = parent.offsetAt(location.range.start);
            let prefixText = (_b = location.prefixText) === null || _b === void 0 ? void 0 : _b.trimRight();
            // rename needs to be prefixed in case of a bind shorthand on a HTML element
            if (!prefixText) {
                const original = parent.getText({
                    start: vscode_languageserver_1.Position.create(location.range.start.line, location.range.start.character - bind.length),
                    end: location.range.end
                });
                if (original.startsWith(bind) &&
                    (0, documents_1.getNodeIfIsInHTMLStartTag)(parent.html, rangeStart)) {
                    return {
                        ...location,
                        prefixText: original.slice(bind.length) + '={',
                        suffixText: '}'
                    };
                }
            }
            if (!prefixText || prefixText.slice(-1) !== ':') {
                return location;
            }
            // prefix is of the form `oldVarName: ` -> hints at a shorthand
            // we need to make sure we only adjust shorthands on elements/components
            if (!(0, documents_1.getNodeIfIsInStartTag)(parent.html, rangeStart) ||
                // shorthands: let:xx, bind:xx, {xx}
                (parent.getText().charAt(rangeStart - 1) !== ':' &&
                    // not use:action={{foo}}
                    !/[^{]\s+{$/.test(parent.getText({
                        start: vscode_languageserver_1.Position.create(0, 0),
                        end: location.range.start
                    })))) {
                return location;
            }
            prefixText = prefixText.slice(0, -1) + '={';
            location = {
                ...location,
                prefixText,
                suffixText: '}'
            };
            // rename range needs to be adjusted in case of an attribute shorthand
            if (snapshot.getOriginalText().charAt(rangeStart - 1) === '{') {
                rangeStart--;
                const rangeEnd = parent.offsetAt(location.range.end) + 1;
                location.range = {
                    start: parent.positionAt(rangeStart),
                    end: parent.positionAt(rangeEnd)
                };
            }
            return location;
        });
    }
}
exports.RenameProviderImpl = RenameProviderImpl;
//# sourceMappingURL=RenameProvider.js.map