"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLinkRenameEdit = exports.getFilePathRange = exports.getLinkRenameText = exports.MdRenameProvider = exports.RenameNotSupportedAtLocationError = void 0;
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
const l10n = require("@vscode/l10n");
const path = require("path");
const vscode_uri_1 = require("vscode-uri");
const config_1 = require("../config");
const logging_1 = require("../logging");
const position_1 = require("../types/position");
const range_1 = require("../types/range");
const textDocument_1 = require("../types/textDocument");
const dispose_1 = require("../util/dispose");
const editBuilder_1 = require("../util/editBuilder");
const mdLinks_1 = require("../util/mdLinks");
const path_1 = require("../util/path");
const uri_1 = require("../util/uri");
const workspace_1 = require("../workspace");
const documentLinks_1 = require("./documentLinks");
const references_1 = require("./references");
/**
 * Error thrown when rename is not supported performed at the requested location.
 */
class RenameNotSupportedAtLocationError extends Error {
    constructor() {
        super(l10n.t('Renaming is not supported here. Try renaming a header or link.'));
    }
}
exports.RenameNotSupportedAtLocationError = RenameNotSupportedAtLocationError;
class MdRenameProvider extends dispose_1.Disposable {
    #cachedRefs;
    #configuration;
    #workspace;
    #referencesProvider;
    #slugifier;
    #logger;
    constructor(configuration, workspace, referencesProvider, slugifier, logger) {
        super();
        this.#configuration = configuration;
        this.#workspace = workspace;
        this.#referencesProvider = referencesProvider;
        this.#slugifier = slugifier;
        this.#logger = logger;
    }
    async prepareRename(document, position, token) {
        this.#logger.log(logging_1.LogLevel.Debug, 'RenameProvider.prepareRename', { document: document.uri, version: document.version });
        const allRefsInfo = await this.#getAllReferences(document, position, token);
        if (token.isCancellationRequested) {
            return undefined;
        }
        if (!allRefsInfo || !allRefsInfo.references.length) {
            throw new RenameNotSupportedAtLocationError();
        }
        const triggerRef = allRefsInfo.triggerRef;
        switch (triggerRef.kind) {
            case references_1.MdReferenceKind.Header: {
                return { range: triggerRef.headerTextLocation.range, placeholder: triggerRef.headerText };
            }
            case references_1.MdReferenceKind.Link: {
                if (triggerRef.link.kind === documentLinks_1.MdLinkKind.Definition) {
                    // We may have been triggered on the ref or the definition itself
                    if ((0, range_1.rangeContains)(triggerRef.link.ref.range, position)) {
                        return { range: triggerRef.link.ref.range, placeholder: triggerRef.link.ref.text };
                    }
                }
                if (triggerRef.link.href.kind === documentLinks_1.HrefKind.External) {
                    return { range: triggerRef.link.source.hrefRange, placeholder: document.getText(triggerRef.link.source.hrefRange) };
                }
                // See if we are renaming the fragment or the path
                const { fragmentRange } = triggerRef.link.source;
                if (fragmentRange && (0, range_1.rangeContains)(fragmentRange, position)) {
                    const declaration = this.#findHeaderDeclaration(allRefsInfo.references);
                    return {
                        range: fragmentRange,
                        placeholder: declaration ? declaration.headerText : document.getText(fragmentRange),
                    };
                }
                const range = getFilePathRange(triggerRef.link);
                if (!range) {
                    throw new RenameNotSupportedAtLocationError();
                }
                return { range, placeholder: (0, uri_1.tryDecodeUri)(document.getText(range)) };
            }
        }
    }
    #findHeaderDeclaration(references) {
        return references.find(ref => ref.isDefinition && ref.kind === references_1.MdReferenceKind.Header);
    }
    async provideRenameEdits(document, position, newName, token) {
        this.#logger.log(logging_1.LogLevel.Debug, 'RenameProvider.provideRenameEdits', { document: document.uri, version: document.version });
        const allRefsInfo = await this.#getAllReferences(document, position, token);
        if (token.isCancellationRequested || !allRefsInfo || !allRefsInfo.references.length) {
            return undefined;
        }
        const triggerRef = allRefsInfo.triggerRef;
        if (triggerRef.kind === references_1.MdReferenceKind.Link && ((triggerRef.link.kind === documentLinks_1.MdLinkKind.Definition && (0, range_1.rangeContains)(triggerRef.link.ref.range, position)) || triggerRef.link.href.kind === documentLinks_1.HrefKind.Reference)) {
            return this.#renameReferenceLinks(allRefsInfo, newName);
        }
        else if (triggerRef.kind === references_1.MdReferenceKind.Link && triggerRef.link.href.kind === documentLinks_1.HrefKind.External) {
            return this.#renameExternalLink(allRefsInfo, newName);
        }
        else if (triggerRef.kind === references_1.MdReferenceKind.Header || (triggerRef.kind === references_1.MdReferenceKind.Link && triggerRef.link.source.fragmentRange && (0, range_1.rangeContains)(triggerRef.link.source.fragmentRange, position) && (triggerRef.link.kind === documentLinks_1.MdLinkKind.Definition || triggerRef.link.kind === documentLinks_1.MdLinkKind.Link && triggerRef.link.href.kind === documentLinks_1.HrefKind.Internal))) {
            return this.#renameFragment(allRefsInfo, newName);
        }
        else if (triggerRef.kind === references_1.MdReferenceKind.Link && !(triggerRef.link.source.fragmentRange && (0, range_1.rangeContains)(triggerRef.link.source.fragmentRange, position)) && (triggerRef.link.kind === documentLinks_1.MdLinkKind.Link || triggerRef.link.kind === documentLinks_1.MdLinkKind.Definition) && triggerRef.link.href.kind === documentLinks_1.HrefKind.Internal) {
            return this.#renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName, token);
        }
        return undefined;
    }
    async #renameFilePath(triggerDocument, triggerHref, allRefsInfo, newName, token) {
        const builder = new editBuilder_1.WorkspaceEditBuilder();
        const targetUri = await (0, workspace_1.statLinkToMarkdownFile)(this.#configuration, this.#workspace, triggerHref.path) ?? triggerHref.path;
        if (token.isCancellationRequested) {
            return builder.getEdit();
        }
        const rawNewFilePath = (0, documentLinks_1.resolveInternalDocumentLink)(triggerDocument, newName, this.#workspace);
        if (!rawNewFilePath) {
            return builder.getEdit();
        }
        let resolvedNewFilePath = rawNewFilePath.resource;
        if (!vscode_uri_1.Utils.extname(resolvedNewFilePath)) {
            // If the newly entered path doesn't have a file extension but the original link did
            // tack on a .md file extension
            if (vscode_uri_1.Utils.extname(targetUri)) {
                resolvedNewFilePath = resolvedNewFilePath.with({
                    path: resolvedNewFilePath.path + '.' + (this.#configuration.markdownFileExtensions[0] ?? config_1.defaultMarkdownFileExtension)
                });
            }
        }
        // First rename the file
        if (await this.#workspace.stat(targetUri)) {
            builder.renameFile(targetUri, resolvedNewFilePath);
        }
        // Then update all refs to it
        for (const ref of allRefsInfo.references) {
            if (ref.kind === references_1.MdReferenceKind.Link) {
                const { range, newText } = this.#getLinkRenameEdit(ref, rawNewFilePath, newName);
                builder.replace(ref.link.source.resource, range, newText);
            }
        }
        return builder.getEdit();
    }
    #getLinkRenameEdit(ref, rawNewFilePath, newName) {
        // Try to preserve style of existing links
        const newLinkText = getLinkRenameText(this.#workspace, ref.link.source, rawNewFilePath.resource, newName.startsWith('./') || newName.startsWith('.\\'));
        return getLinkRenameEdit(ref.link, newLinkText ?? newName);
    }
    #renameFragment(allRefsInfo, newName) {
        const slug = this.#slugifier.fromHeading(newName).value;
        const builder = new editBuilder_1.WorkspaceEditBuilder();
        for (const ref of allRefsInfo.references) {
            switch (ref.kind) {
                case references_1.MdReferenceKind.Header:
                    builder.replace(vscode_uri_1.URI.parse(ref.location.uri), ref.headerTextLocation.range, newName);
                    break;
                case references_1.MdReferenceKind.Link:
                    builder.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, !ref.link.source.fragmentRange || ref.link.href.kind === documentLinks_1.HrefKind.External ? newName : slug);
                    break;
            }
        }
        return builder.getEdit();
    }
    #renameExternalLink(allRefsInfo, newName) {
        const builder = new editBuilder_1.WorkspaceEditBuilder();
        for (const ref of allRefsInfo.references) {
            if (ref.kind === references_1.MdReferenceKind.Link) {
                builder.replace(ref.link.source.resource, ref.location.range, newName);
            }
        }
        return builder.getEdit();
    }
    #renameReferenceLinks(allRefsInfo, newName) {
        const builder = new editBuilder_1.WorkspaceEditBuilder();
        for (const ref of allRefsInfo.references) {
            if (ref.kind === references_1.MdReferenceKind.Link) {
                if (ref.link.kind === documentLinks_1.MdLinkKind.Definition) {
                    builder.replace(ref.link.source.resource, ref.link.ref.range, newName);
                }
                else {
                    builder.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, newName);
                }
            }
        }
        return builder.getEdit();
    }
    async #getAllReferences(document, position, token) {
        const version = document.version;
        if (this.#cachedRefs
            && this.#cachedRefs.resource.fsPath === (0, textDocument_1.getDocUri)(document).fsPath
            && this.#cachedRefs.version === document.version
            && (0, position_1.arePositionsEqual)(this.#cachedRefs.position, position)) {
            return this.#cachedRefs;
        }
        const references = await this.#referencesProvider.getReferencesAtPosition(document, position, token);
        if (token.isCancellationRequested) {
            return;
        }
        const triggerRef = references.find(ref => ref.isTriggerLocation);
        if (!triggerRef) {
            return undefined;
        }
        this.#cachedRefs = {
            resource: (0, textDocument_1.getDocUri)(document),
            version,
            position,
            references,
            triggerRef
        };
        return this.#cachedRefs;
    }
}
exports.MdRenameProvider = MdRenameProvider;
function getLinkRenameText(workspace, source, newPath, preferDotSlash = false) {
    if (source.hrefText.startsWith('/')) {
        const root = (0, documentLinks_1.resolveInternalDocumentLink)(source.resource, '/', workspace);
        if (!root) {
            return undefined;
        }
        return '/' + path.posix.relative(root.resource.path, newPath.path);
    }
    return (0, path_1.computeRelativePath)(source.resource, newPath, preferDotSlash);
}
exports.getLinkRenameText = getLinkRenameText;
function getFilePathRange(link) {
    if (link.source.fragmentRange) {
        return (0, range_1.modifyRange)(link.source.hrefRange, undefined, (0, position_1.translatePosition)(link.source.fragmentRange.start, { characterDelta: -1 }));
    }
    return link.source.hrefRange;
}
exports.getFilePathRange = getFilePathRange;
function newPathWithFragmentIfNeeded(newPath, link) {
    if (link.href.kind === documentLinks_1.HrefKind.Internal && link.href.fragment) {
        return newPath + '#' + link.href.fragment;
    }
    return newPath;
}
function getLinkRenameEdit(link, newPathText) {
    const linkRange = link.source.hrefRange;
    // TODO: this won't be correct if the file name contains `\`
    newPathText = newPathWithFragmentIfNeeded(newPathText.replace(/\\/g, '/'), link);
    if (link.source.isAngleBracketLink) {
        if (!(0, mdLinks_1.needsAngleBracketLink)(newPathText)) {
            // Remove the angle brackets
            const range = (0, range_1.makeRange)((0, position_1.translatePosition)(linkRange.start, { characterDelta: -1 }), (0, position_1.translatePosition)(linkRange.end, { characterDelta: 1 }));
            return { range, newText: newPathText };
        }
        else {
            return { range: linkRange, newText: (0, mdLinks_1.escapeForAngleBracketLink)(newPathText) };
        }
    }
    // We might need to use angle brackets for the link
    if ((0, mdLinks_1.needsAngleBracketLink)(newPathText)) {
        return { range: linkRange, newText: `<${(0, mdLinks_1.escapeForAngleBracketLink)(newPathText)}>` };
    }
    return { range: linkRange, newText: newPathText };
}
exports.getLinkRenameEdit = getLinkRenameEdit;
//# sourceMappingURL=rename.js.map