"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MdPathCompletionProvider = exports.IncludeWorkspaceHeaderCompletions = void 0;
const l10n = require("@vscode/l10n");
const path_1 = require("path");
const lsp = require("vscode-languageserver-types");
const vscode_uri_1 = require("vscode-uri");
const config_1 = require("../config");
const tableOfContents_1 = require("../tableOfContents");
const position_1 = require("../types/position");
const range_1 = require("../types/range");
const textDocument_1 = require("../types/textDocument");
const file_1 = require("../util/file");
const path_2 = require("../util/path");
const schemes_1 = require("../util/schemes");
const string_1 = require("../util/string");
const workspace_1 = require("../workspace");
const workspaceCache_1 = require("../workspaceCache");
const documentLinks_1 = require("./documentLinks");
const mdLinks_1 = require("../util/mdLinks");
var CompletionContextKind;
(function (CompletionContextKind) {
    /** `[...](|)` */
    CompletionContextKind[CompletionContextKind["Link"] = 0] = "Link";
    /** `[...][|]` */
    CompletionContextKind[CompletionContextKind["ReferenceLink"] = 1] = "ReferenceLink";
    /** `[]: |` */
    CompletionContextKind[CompletionContextKind["LinkDefinition"] = 2] = "LinkDefinition";
    /** <img src="|"> */
    CompletionContextKind[CompletionContextKind["HtmlAttribute"] = 3] = "HtmlAttribute";
})(CompletionContextKind || (CompletionContextKind = {}));
function tryDecodeUriComponent(str) {
    try {
        return decodeURIComponent(str);
    }
    catch {
        return str;
    }
}
/**
 * Controls if header completions for other files in the workspace be returned.
 */
var IncludeWorkspaceHeaderCompletions;
(function (IncludeWorkspaceHeaderCompletions) {
    /**
     * Never return workspace header completions.
     */
    IncludeWorkspaceHeaderCompletions["never"] = "never";
    /**
     * Return workspace header completions after `##` is typed.
     *
     * This lets the user signal
     */
    IncludeWorkspaceHeaderCompletions["onDoubleHash"] = "onDoubleHash";
    /**
     * Return workspace header completions after either a single `#` is typed or after `##`
     *
     * For a single hash, this means the workspace header completions will be returned along side the current file header completions.
     */
    IncludeWorkspaceHeaderCompletions["onSingleOrDoubleHash"] = "onSingleOrDoubleHash";
})(IncludeWorkspaceHeaderCompletions || (exports.IncludeWorkspaceHeaderCompletions = IncludeWorkspaceHeaderCompletions = {}));
const sortTexts = Object.freeze({
    localHeader: '1',
    workspaceHeader: '2',
});
/**
 * Adds path completions in markdown files.
 */
class MdPathCompletionProvider {
    #configuration;
    #workspace;
    #parser;
    #linkProvider;
    #workspaceTocCache;
    constructor(configuration, workspace, parser, linkProvider, tocProvider) {
        this.#configuration = configuration;
        this.#workspace = workspace;
        this.#parser = parser;
        this.#linkProvider = linkProvider;
        this.#workspaceTocCache = new workspaceCache_1.MdWorkspaceInfoCache(workspace, (doc) => tocProvider.getForDocument(doc));
    }
    async provideCompletionItems(document, position, context, token) {
        const pathContext = this.#getPathCompletionContext(document, position);
        if (!pathContext) {
            return [];
        }
        const items = [];
        for await (const item of this.#provideCompletionItems(document, position, pathContext, context, token)) {
            items.push(item);
        }
        return items;
    }
    async *#provideCompletionItems(document, position, context, options, token) {
        switch (context.kind) {
            case CompletionContextKind.ReferenceLink: {
                yield* this.#provideReferenceSuggestions(document, position, context, token);
                return;
            }
            case CompletionContextKind.LinkDefinition:
            case CompletionContextKind.Link:
            case CompletionContextKind.HtmlAttribute: {
                if ((context.linkPrefix.startsWith('#') && options.includeWorkspaceHeaderCompletions === IncludeWorkspaceHeaderCompletions.onSingleOrDoubleHash) ||
                    (context.linkPrefix.startsWith('##') && (options.includeWorkspaceHeaderCompletions === IncludeWorkspaceHeaderCompletions.onDoubleHash || options.includeWorkspaceHeaderCompletions === IncludeWorkspaceHeaderCompletions.onSingleOrDoubleHash))) {
                    const insertRange = (0, range_1.makeRange)(context.linkTextStartPosition, position);
                    yield* this.#provideWorkspaceHeaderSuggestions(document, position, context, insertRange, token);
                    return;
                }
                const isAnchorInCurrentDoc = context.anchorInfo && context.anchorInfo.beforeAnchor.length === 0;
                // Add anchor #links in current doc
                if (context.linkPrefix.length === 0 || isAnchorInCurrentDoc) {
                    const insertRange = (0, range_1.makeRange)(context.linkTextStartPosition, position);
                    yield* this.#provideHeaderSuggestions(document, position, context, insertRange, token);
                }
                if (token.isCancellationRequested) {
                    return;
                }
                if (!isAnchorInCurrentDoc) {
                    if (context.anchorInfo) { // Anchor to a different document
                        const rawUri = this.#resolveReference(document, context.anchorInfo.beforeAnchor);
                        if (rawUri) {
                            const otherDoc = await (0, workspace_1.openLinkToMarkdownFile)(this.#configuration, this.#workspace, rawUri);
                            if (token.isCancellationRequested) {
                                return;
                            }
                            if (otherDoc) {
                                const anchorStartPosition = (0, position_1.translatePosition)(position, { characterDelta: -(context.anchorInfo.anchorPrefix.length + 1) });
                                const range = (0, range_1.makeRange)(anchorStartPosition, position);
                                yield* this.#provideHeaderSuggestions(otherDoc, position, context, range, token);
                            }
                        }
                    }
                    else { // Normal path suggestions
                        yield* this.#providePathSuggestions(document, position, context, token);
                    }
                }
                return;
            }
        }
    }
    /// [...](...|
    #linkStartPattern = new RegExp(
    // text
    (0, string_1.r) `\[` +
        /**/ (0, string_1.r) `(?:` +
        /*****/ (0, string_1.r) `[^\[\]\\]|` + // Non-bracket chars, or...
        /*****/ (0, string_1.r) `\\.|` + // Escaped char, or...
        /*****/ (0, string_1.r) `\[[^\[\]]*\]` + // Matched bracket pair
        /**/ (0, string_1.r) `)*` +
        (0, string_1.r) `\]` +
        // Destination start
        (0, string_1.r) `\(\s*(<[^\>\)]*|[^\s\(\)]*)` +
        (0, string_1.r) `$` // Must match cursor position
    );
    /// [...][...|
    #referenceLinkStartPattern = /\[([^\]]*?)\]\[\s*([^\s\(\)]*)$/;
    /// [id]: |
    #definitionPattern = /^\s*\[[\w\-]+\]:\s*([^\s]*)$/m;
    /// [id]: |
    #htmlAttributeStartPattern = /\<(?<tag>\w+)([^>]*)\s(?<attr>\w+)=['"](?<link>[^'"]*)$/m;
    #getPathCompletionContext(document, position) {
        const line = (0, textDocument_1.getLine)(document, position.line);
        const linePrefixText = line.slice(0, position.character);
        const lineSuffixText = line.slice(position.character);
        const htmlAttributePrefixMatch = linePrefixText.match(this.#htmlAttributeStartPattern);
        if (htmlAttributePrefixMatch?.groups) {
            const validPathAttrs = documentLinks_1.htmlTagPathAttrs.get(htmlAttributePrefixMatch.groups.tag.toUpperCase());
            if (!validPathAttrs || !validPathAttrs.some(attr => attr === htmlAttributePrefixMatch.groups.attr.toLowerCase())) {
                return undefined;
            }
            const prefix = htmlAttributePrefixMatch.groups.link;
            if (this.#refLooksLikeUrl(prefix)) {
                return undefined;
            }
            const suffix = lineSuffixText.match(/^[^\s'"]*/);
            return this.#createCompletionContext(CompletionContextKind.HtmlAttribute, position, prefix, suffix?.[0] ?? '', false);
        }
        const linkPrefixMatch = linePrefixText.match(this.#linkStartPattern);
        if (linkPrefixMatch) {
            const isAngleBracketLink = linkPrefixMatch[1].startsWith('<');
            const prefix = linkPrefixMatch[1].slice(isAngleBracketLink ? 1 : 0);
            if (this.#refLooksLikeUrl(prefix)) {
                return undefined;
            }
            const suffix = lineSuffixText.match(/^[^\)\s][^\)\s\>]*/);
            return this.#createCompletionContext(CompletionContextKind.Link, position, prefix, suffix?.[0] ?? '', isAngleBracketLink);
        }
        const definitionLinkPrefixMatch = linePrefixText.match(this.#definitionPattern);
        if (definitionLinkPrefixMatch) {
            const isAngleBracketLink = definitionLinkPrefixMatch[1].startsWith('<');
            const prefix = definitionLinkPrefixMatch[1].slice(isAngleBracketLink ? 1 : 0);
            if (this.#refLooksLikeUrl(prefix)) {
                return undefined;
            }
            const suffix = lineSuffixText.match(/^[^\s]*/);
            return this.#createCompletionContext(CompletionContextKind.LinkDefinition, position, prefix, suffix?.[0] ?? '', isAngleBracketLink);
        }
        const referenceLinkPrefixMatch = linePrefixText.match(this.#referenceLinkStartPattern);
        if (referenceLinkPrefixMatch) {
            const prefix = referenceLinkPrefixMatch[2];
            const suffix = lineSuffixText.match(/^[^\]\s]*/);
            return {
                kind: CompletionContextKind.ReferenceLink,
                linkPrefix: prefix,
                linkTextStartPosition: (0, position_1.translatePosition)(position, { characterDelta: -prefix.length }),
                linkSuffix: suffix ? suffix[0] : '',
            };
        }
        return undefined;
    }
    #createCompletionContext(kind, position, prefix, suffix, isAngleBracketPath) {
        return {
            kind,
            linkPrefix: tryDecodeUriComponent(prefix),
            linkTextStartPosition: (0, position_1.translatePosition)(position, { characterDelta: -prefix.length }),
            linkSuffix: suffix,
            anchorInfo: this.#getAnchorContext(prefix),
            isAngleBracketPath,
        };
    }
    /**
     * Check if {@param ref} looks like a 'http:' style url.
     */
    #refLooksLikeUrl(prefix) {
        return /^\s*[\w\d\-]+:/.test(prefix);
    }
    #getAnchorContext(prefix) {
        const anchorMatch = prefix.match(/^(.*)#([\w\d\-]*)$/);
        if (!anchorMatch) {
            return undefined;
        }
        return {
            beforeAnchor: anchorMatch[1],
            anchorPrefix: anchorMatch[2],
        };
    }
    async *#provideReferenceSuggestions(document, position, context, token) {
        const insertionRange = (0, range_1.makeRange)(context.linkTextStartPosition, position);
        const replacementRange = (0, range_1.makeRange)(insertionRange.start, (0, position_1.translatePosition)(position, { characterDelta: context.linkSuffix.length }));
        const { definitions } = await this.#linkProvider.getLinks(document);
        if (token.isCancellationRequested) {
            return;
        }
        for (const def of definitions) {
            yield {
                kind: lsp.CompletionItemKind.Reference,
                label: def.ref.text,
                detail: l10n.t(`Reference link '{0}'`, def.ref.text),
                textEdit: {
                    newText: def.ref.text,
                    insert: insertionRange,
                    replace: replacementRange,
                },
            };
        }
    }
    async *#provideHeaderSuggestions(document, position, context, insertionRange, token) {
        const toc = await tableOfContents_1.TableOfContents.createForContainingDoc(this.#parser, this.#workspace, document, token);
        if (token.isCancellationRequested) {
            return;
        }
        const replacementRange = (0, range_1.makeRange)(insertionRange.start, (0, position_1.translatePosition)(position, { characterDelta: context.linkSuffix.length }));
        for (const entry of toc.entries) {
            const completionItem = this.#createHeaderCompletion(entry, insertionRange, replacementRange);
            completionItem.labelDetails = {};
            yield completionItem;
        }
    }
    #createHeaderCompletion(entry, insertionRange, replacementRange, filePath = '') {
        const label = '#' + decodeURIComponent(entry.slug.value);
        const newText = filePath + '#' + decodeURIComponent(entry.slug.value);
        return {
            kind: lsp.CompletionItemKind.Reference,
            label,
            detail: this.#ownHeaderEntryDetails(entry),
            textEdit: {
                newText,
                insert: insertionRange,
                replace: replacementRange,
            },
        };
    }
    #ownHeaderEntryDetails(entry) {
        return l10n.t(`Link to '{0}'`, '#'.repeat(entry.level) + ' ' + entry.text);
    }
    /**
     * Suggestions for headers across  all md files in the workspace
     */
    async *#provideWorkspaceHeaderSuggestions(document, position, context, insertionRange, token) {
        const tocs = await this.#workspaceTocCache.entries();
        if (token.isCancellationRequested) {
            return;
        }
        const replacementRange = (0, range_1.makeRange)(insertionRange.start, (0, position_1.translatePosition)(position, { characterDelta: context.linkSuffix.length }));
        for (const [toDoc, toc] of tocs) {
            const isHeaderInCurrentDocument = toDoc.toString() === (0, textDocument_1.getDocUri)(document).toString();
            const rawPath = isHeaderInCurrentDocument ? '' : (0, path_2.computeRelativePath)((0, textDocument_1.getDocUri)(document), toDoc);
            if (typeof rawPath === 'undefined') {
                continue;
            }
            const normalizedPath = this.#normalizeFileNameCompletion(rawPath);
            const path = this.#getPathInsertText(context, normalizedPath);
            for (const entry of toc.entries) {
                const completionItem = this.#createHeaderCompletion(entry, insertionRange, replacementRange, path);
                completionItem.filterText = '#' + completionItem.label;
                completionItem.sortText = isHeaderInCurrentDocument ? sortTexts.localHeader : sortTexts.workspaceHeader;
                if (isHeaderInCurrentDocument) {
                    completionItem.detail = this.#ownHeaderEntryDetails(entry);
                }
                else if (path) {
                    completionItem.detail = l10n.t(`Link to '# {0}' in '{1}'`, entry.text, path);
                    completionItem.labelDetails = { description: path };
                }
                yield completionItem;
            }
        }
    }
    async *#providePathSuggestions(document, position, context, token) {
        const valueBeforeLastSlash = context.linkPrefix.substring(0, context.linkPrefix.lastIndexOf('/') + 1); // keep the last slash
        const parentDir = this.#resolveReference(document, valueBeforeLastSlash || '.');
        if (!parentDir) {
            return;
        }
        const pathSegmentStart = (0, position_1.translatePosition)(position, { characterDelta: valueBeforeLastSlash.length - context.linkPrefix.length });
        const insertRange = (0, range_1.makeRange)(pathSegmentStart, position);
        const pathSegmentEnd = (0, position_1.translatePosition)(position, { characterDelta: context.linkSuffix.length });
        const replacementRange = (0, range_1.makeRange)(pathSegmentStart, pathSegmentEnd);
        let dirInfo;
        try {
            dirInfo = await this.#workspace.readDirectory(parentDir);
        }
        catch {
            return;
        }
        if (token.isCancellationRequested) {
            return;
        }
        // eslint-disable-next-line prefer-const
        for (let [name, type] of dirInfo) {
            const uri = vscode_uri_1.Utils.joinPath(parentDir, name);
            if ((0, config_1.isExcludedPath)(this.#configuration, uri)) {
                continue;
            }
            if (!type.isDirectory) {
                name = this.#normalizeFileNameCompletion(name);
            }
            const isDir = type.isDirectory;
            const newText = this.#getPathInsertText(context, name) + (isDir ? '/' : '');
            const label = isDir ? name + '/' : name;
            yield {
                label,
                kind: isDir ? lsp.CompletionItemKind.Folder : lsp.CompletionItemKind.File,
                detail: l10n.t(`Link to '{0}'`, label),
                documentation: isDir ? uri.path + '/' : uri.path,
                textEdit: {
                    newText,
                    insert: insertRange,
                    replace: replacementRange,
                },
                command: isDir ? { command: 'editor.action.triggerSuggest', title: '' } : undefined,
            };
        }
    }
    #getPathInsertText(context, name) {
        // If the  path has a literal `%` in it, we need encode it to prevent
        // it being incorrectly decoded
        name = name.replaceAll('%', '%25');
        if (context.kind === CompletionContextKind.HtmlAttribute) {
            return name
                .replaceAll(`"`, '&quot;')
                .replaceAll(`'`, '&apos;');
        }
        if (context.isAngleBracketPath) {
            return (0, mdLinks_1.escapeForAngleBracketLink)(name);
        }
        if (!(0, mdLinks_1.hasBalancedParens)(name)) {
            name = name.replace(/([()])/g, '\\$1'); // CodeQL [SM02383] This code is for editing text, not sanitizing rendered markdown
        }
        return name.replaceAll(' ', '%20');
    }
    #normalizeFileNameCompletion(name) {
        if (this.#configuration.preferredMdPathExtensionStyle === 'removeExtension') {
            if ((0, file_1.looksLikeMarkdownFilePath)(this.#configuration, name)) {
                const ext = (0, path_1.extname)(name);
                name = name.slice(0, -ext.length);
            }
        }
        return name;
    }
    #resolveReference(document, ref) {
        const docUri = this.#getFileUriOfTextDocument(document);
        if (ref.startsWith('/')) {
            const workspaceFolder = (0, workspace_1.getWorkspaceFolder)(this.#workspace, docUri);
            if (workspaceFolder) {
                return vscode_uri_1.Utils.joinPath(workspaceFolder, ref);
            }
            else {
                return this.#resolvePath(docUri, ref.slice(1));
            }
        }
        return this.#resolvePath(docUri, ref);
    }
    #resolvePath(root, ref) {
        try {
            if (root.scheme === schemes_1.Schemes.file) {
                return vscode_uri_1.URI.file((0, path_1.resolve)((0, path_1.dirname)(root.fsPath), ref));
            }
            else {
                return root.with({
                    path: (0, path_1.resolve)((0, path_1.dirname)(root.path), ref),
                });
            }
        }
        catch {
            return undefined;
        }
    }
    #getFileUriOfTextDocument(document) {
        return this.#workspace.getContainingDocument?.((0, textDocument_1.getDocUri)(document))?.uri ?? (0, textDocument_1.getDocUri)(document);
    }
}
exports.MdPathCompletionProvider = MdPathCompletionProvider;
//# sourceMappingURL=pathCompletions.js.map