"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HTMLPlugin = void 0;
const emmet_helper_1 = require("@vscode/emmet-helper");
const vscode_html_languageservice_1 = require("vscode-html-languageservice");
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../lib/documents");
const dataProvider_1 = require("./dataProvider");
const utils_1 = require("../../lib/documents/utils");
const utils_2 = require("../../utils");
const importPackage_1 = require("../../importPackage");
const path_1 = __importDefault(require("path"));
const logger_1 = require("../../logger");
const indentFolding_1 = require("../../lib/foldingRange/indentFolding");
const wordHighlight_1 = require("../../lib/documentHighlight/wordHighlight");
const vscode_uri_1 = require("vscode-uri");
const getFoldingRanges_1 = require("./getFoldingRanges");
// https://github.com/microsoft/vscode/blob/c6f507deeb99925e713271b1048f21dbaab4bd54/extensions/html/language-configuration.json#L34
const wordPattern = /(-?\d*\.\d\w*)|([^`~!@$^&*()=+[{\]}\|;:'",.<>\/\s]+)/g;
const attributeValuePlaceHolder = '="$1"';
class HTMLPlugin {
    constructor(docManager, configManager, fileSystemProvider, workspaceFolders) {
        this.configManager = configManager;
        this.fileSystemProvider = fileSystemProvider;
        this.workspaceFolders = workspaceFolders;
        this.__name = 'html';
        this.lang = (0, vscode_html_languageservice_1.getLanguageService)({
            customDataProviders: this.getCustomDataProviders(),
            useDefaultDataProvider: false,
            clientCapabilities: this.configManager.getClientCapabilities()
        });
        this.documents = new WeakMap();
        this.styleScriptTemplate = new Set(['template', 'style', 'script']);
        this.htmlTriggerCharacters = ['.', ':', '<', '"', '=', '/'];
        this.loadedEmmetExtensionsPaths = '';
        configManager.onChange(() => {
            this.lang.setDataProviders(false, this.getCustomDataProviders());
            this.reloadEmmetExtensionsIfNeeded();
        });
        this.reloadEmmetExtensionsIfNeeded();
        const sync = (document) => {
            this.documents.set(document, document.html);
        };
        docManager.on('documentChange', sync);
        docManager.on('documentOpen', sync);
    }
    reloadEmmetExtensionsIfNeeded() {
        const emmetConfig = this.configManager.getEmmetConfig();
        const extensionsPath = emmetConfig.extensionsPath || [];
        const joinedPaths = Array.from(extensionsPath).sort().join(',');
        if (this.loadedEmmetExtensionsPaths !== joinedPaths) {
            this.loadedEmmetExtensionsPaths = joinedPaths;
            (0, emmet_helper_1.updateExtensionsPath)(extensionsPath, this.fileSystemProvider, this.workspaceFolders.map((folder) => vscode_uri_1.URI.parse(folder.uri))).catch((err) => logger_1.Logger.error(err));
        }
    }
    doHover(document, position) {
        if (!this.featureEnabled('hover')) {
            return null;
        }
        const html = this.documents.get(document);
        if (!html) {
            return null;
        }
        const node = html.findNodeAt(document.offsetAt(position));
        if (!node || (0, utils_2.possiblyComponent)(node)) {
            return null;
        }
        return this.lang.doHover(document, position, html);
    }
    async getCompletions(document, position, completionContext) {
        if (!this.featureEnabled('completions')) {
            return null;
        }
        const html = this.documents.get(document);
        if (!html) {
            return null;
        }
        if (this.isInsideMoustacheTag(html, document, position) ||
            (0, documents_1.isInTag)(position, document.scriptInfo) ||
            (0, documents_1.isInTag)(position, document.moduleScriptInfo)) {
            return null;
        }
        let emmetResults = {
            isIncomplete: false,
            items: []
        };
        let doEmmetCompleteInner = () => null;
        if (this.configManager.getConfig().html.completions.emmet &&
            this.configManager.getEmmetConfig().showExpandedAbbreviation !== 'never') {
            doEmmetCompleteInner = () => (0, emmet_helper_1.doComplete)(document, position, 'html', this.configManager.getEmmetConfig());
            this.lang.setCompletionParticipants([
                {
                    onHtmlContent: () => (emmetResults = doEmmetCompleteInner() || emmetResults)
                }
            ]);
        }
        if (completionContext?.triggerCharacter &&
            !this.htmlTriggerCharacters.includes(completionContext?.triggerCharacter)) {
            const node = html.findNodeAt(document.offsetAt(position));
            const offset = document.offsetAt(position);
            if (!node?.tag ||
                (offset > (node.startTagEnd ?? node.end) &&
                    (node.endTagStart == null || offset <= node.endTagStart))) {
                return doEmmetCompleteInner() ?? null;
            }
            return null;
        }
        const results = this.isInComponentTag(html, document, position)
            ? // Only allow emmet inside component element tags.
                // Other attributes/events would be false positives.
                vscode_languageserver_1.CompletionList.create([])
            : this.lang.doComplete(document, position, html);
        const items = this.toCompletionItems(results.items);
        const filePath = document.getFilePath();
        const prettierConfig = filePath &&
            items.some((item) => item.label.startsWith('on:') || item.label.startsWith('bind:'))
            ? this.configManager.getMergedPrettierConfig(await (0, importPackage_1.importPrettier)(filePath).resolveConfig(filePath, this.configManager.getPrettierConfigLoadingOptions()))
            : null;
        const svelteStrictMode = prettierConfig?.svelteStrictMode;
        const startQuote = svelteStrictMode ? '"{' : '{';
        const endQuote = svelteStrictMode ? '}"' : '}';
        items.forEach((item) => {
            if (item.label.endsWith(':')) {
                item.kind = vscode_languageserver_1.CompletionItemKind.Keyword;
                if (item.textEdit) {
                    item.textEdit.newText = item.textEdit.newText.replace(attributeValuePlaceHolder, '');
                }
            }
            if (!item.textEdit) {
                return;
            }
            if (item.label.startsWith('on')) {
                const isLegacyDirective = item.label.startsWith('on:');
                const modifierTabStop = isLegacyDirective ? '$2' : '';
                item.textEdit = {
                    ...item.textEdit,
                    newText: item.textEdit.newText.replace(attributeValuePlaceHolder, `${modifierTabStop}=${startQuote}$1${endQuote}`)
                };
                // In Svelte 5, people should use `onclick` instead of `on:click`
                if (isLegacyDirective && document.isSvelte5) {
                    item.sortText = 'z' + (item.sortText ?? item.label);
                }
            }
            if (item.label.startsWith('bind:')) {
                item.textEdit = {
                    ...item.textEdit,
                    newText: item.textEdit.newText.replace(attributeValuePlaceHolder, `=${startQuote}$1${endQuote}`)
                };
            }
        });
        return vscode_languageserver_1.CompletionList.create([...items, ...this.getLangCompletions(items), ...emmetResults.items], 
        // Emmet completions change on every keystroke, so they are never complete
        emmetResults.items.length > 0);
    }
    /**
     * The HTML language service uses newer types which clash
     * without the stable ones. Transform to the stable types.
     */
    toCompletionItems(items) {
        return items.map((item) => {
            if (!item.textEdit || vscode_languageserver_1.TextEdit.is(item.textEdit)) {
                return item;
            }
            return {
                ...item,
                textEdit: vscode_languageserver_1.TextEdit.replace(item.textEdit.replace, item.textEdit.newText)
            };
        });
    }
    isInComponentTag(html, document, position) {
        return !!(0, documents_1.getNodeIfIsInComponentStartTag)(html, document, document.offsetAt(position));
    }
    getLangCompletions(completions) {
        const styleScriptTemplateCompletions = completions.filter((completion) => completion.kind === vscode_languageserver_1.CompletionItemKind.Property &&
            this.styleScriptTemplate.has(completion.label));
        const langCompletions = [];
        addLangCompletion('script', ['ts']);
        addLangCompletion('style', ['less', 'scss']);
        addLangCompletion('template', ['pug']);
        return langCompletions;
        function addLangCompletion(tag, languages) {
            const existingCompletion = styleScriptTemplateCompletions.find((completion) => completion.label === tag);
            if (!existingCompletion) {
                return;
            }
            languages.forEach((lang) => langCompletions.push({
                ...existingCompletion,
                label: `${tag} (lang="${lang}")`,
                insertText: existingCompletion.insertText &&
                    `${existingCompletion.insertText} lang="${lang}"`,
                textEdit: existingCompletion.textEdit && vscode_languageserver_1.TextEdit.is(existingCompletion.textEdit)
                    ? {
                        range: existingCompletion.textEdit.range,
                        newText: `${existingCompletion.textEdit.newText} lang="${lang}"`
                    }
                    : undefined
            }));
        }
    }
    doTagComplete(document, position) {
        if (!this.featureEnabled('tagComplete')) {
            return null;
        }
        const html = this.documents.get(document);
        if (!html) {
            return null;
        }
        if (this.isInsideMoustacheTag(html, document, position)) {
            return null;
        }
        return this.lang.doTagComplete(document, position, html);
    }
    isInsideMoustacheTag(html, document, position) {
        const offset = document.offsetAt(position);
        const node = html.findNodeAt(offset);
        return (0, utils_1.isInsideMoustacheTag)(document.getText(), node.start, offset);
    }
    getDocumentSymbols(document) {
        if (!this.featureEnabled('documentSymbols')) {
            return [];
        }
        const html = this.documents.get(document);
        if (!html) {
            return [];
        }
        return this.lang.findDocumentSymbols(document, html);
    }
    rename(document, position, newName) {
        const html = this.documents.get(document);
        if (!html) {
            return null;
        }
        const node = html.findNodeAt(document.offsetAt(position));
        if (!node || (0, utils_2.possiblyComponent)(node)) {
            return null;
        }
        return this.lang.doRename(document, position, newName, html);
    }
    prepareRename(document, position) {
        const html = this.documents.get(document);
        if (!html) {
            return null;
        }
        const offset = document.offsetAt(position);
        const node = html.findNodeAt(offset);
        if (!node || (0, utils_2.possiblyComponent)(node) || !node.tag || !this.isRenameAtTag(node, offset)) {
            return null;
        }
        const tagNameStart = node.start + '<'.length;
        return (0, utils_1.toRange)(document, tagNameStart, tagNameStart + node.tag.length);
    }
    getLinkedEditingRanges(document, position) {
        if (!this.featureEnabled('linkedEditing')) {
            return null;
        }
        const html = this.documents.get(document);
        if (!html) {
            return null;
        }
        const ranges = this.lang.findLinkedEditingRanges(document, position, html);
        if (!ranges) {
            return null;
        }
        // Note that `.` is excluded from the word pattern. This is intentional to support property access in Svelte component tags.
        return {
            ranges,
            wordPattern: '(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\'\\"\\,\\<\\>\\/\\s]+)'
        };
    }
    getFoldingRanges(document) {
        const result = (0, getFoldingRanges_1.getFoldingRanges)(document, this.configManager.getClientCapabilities()?.textDocument?.foldingRange);
        const templateRange = document.templateInfo
            ? (0, indentFolding_1.indentBasedFoldingRangeForTag)(document, document.templateInfo)
            : [];
        return result.concat(templateRange);
    }
    findDocumentHighlight(document, position) {
        const html = this.documents.get(document);
        if (!html) {
            return null;
        }
        const templateResult = (0, wordHighlight_1.wordHighlightForTag)(document, position, document.templateInfo, wordPattern);
        if (templateResult) {
            return templateResult;
        }
        const node = html.findNodeAt(document.offsetAt(position));
        if ((0, utils_2.possiblyComponent)(node)) {
            return null;
        }
        const result = this.lang.findDocumentHighlights(document, position, html);
        if (!result.length) {
            return null;
        }
        return result;
    }
    /**
     * Returns true if rename happens at the tag name, not anywhere inbetween.
     */
    isRenameAtTag(node, offset) {
        if (!node.tag) {
            return false;
        }
        const startTagNameEnd = node.start + `<${node.tag}`.length;
        const isAtStartTag = offset > node.start && offset <= startTagNameEnd;
        const isAtEndTag = node.endTagStart !== undefined && offset >= node.endTagStart && offset < node.end;
        return isAtStartTag || isAtEndTag;
    }
    getCustomDataProviders() {
        const providers = this.configManager
            .getHTMLConfig()
            ?.customData?.map((customDataPath) => {
            try {
                const jsonPath = path_1.default.resolve(customDataPath);
                return (0, vscode_html_languageservice_1.newHTMLDataProvider)(customDataPath, require(jsonPath));
            }
            catch (error) {
                logger_1.Logger.error(error);
            }
        })
            .filter(utils_2.isNotNullOrUndefined) ?? [];
        return [dataProvider_1.svelteHtmlDataProvider].concat(providers);
    }
    featureEnabled(feature) {
        return (this.configManager.enabled('html.enable') &&
            this.configManager.enabled(`html.${feature}.enable`));
    }
}
exports.HTMLPlugin = HTMLPlugin;
//# sourceMappingURL=HTMLPlugin.js.map