"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginHost = void 0;
const lodash_1 = require("lodash");
const perf_hooks_1 = require("perf_hooks");
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../lib/documents");
const logger_1 = require("../logger");
const utils_1 = require("../utils");
var ExecuteMode;
(function (ExecuteMode) {
    ExecuteMode[ExecuteMode["None"] = 0] = "None";
    ExecuteMode[ExecuteMode["FirstNonNull"] = 1] = "FirstNonNull";
    ExecuteMode[ExecuteMode["Collect"] = 2] = "Collect";
})(ExecuteMode || (ExecuteMode = {}));
class PluginHost {
    constructor(documentsManager) {
        this.documentsManager = documentsManager;
        this.plugins = [];
        this.pluginHostConfig = {
            filterIncompleteCompletions: true,
            definitionLinkSupport: false
        };
        this.deferredRequests = {};
        this.requestTimings = {};
    }
    initialize(pluginHostConfig) {
        this.pluginHostConfig = pluginHostConfig;
    }
    register(plugin) {
        this.plugins.push(plugin);
    }
    didUpdateDocument() {
        this.deferredRequests = {};
    }
    async getDiagnostics(textDocument) {
        var _a, _b, _c, _d;
        const document = this.getDocument(textDocument.uri);
        if ((((_a = document.getFilePath()) === null || _a === void 0 ? void 0 : _a.includes('/node_modules/')) ||
            ((_b = document.getFilePath()) === null || _b === void 0 ? void 0 : _b.includes('\\node_modules\\'))) &&
            // Sapper convention: Put stuff inside node_modules below src
            !(((_c = document.getFilePath()) === null || _c === void 0 ? void 0 : _c.includes('/src/node_modules/')) ||
                ((_d = document.getFilePath()) === null || _d === void 0 ? void 0 : _d.includes('\\src\\node_modules\\')))) {
            // Don't return diagnostics for files inside node_modules. These are considered read-only (cannot be changed)
            // and in case of svelte-check they would pollute/skew the output
            return [];
        }
        return (0, lodash_1.flatten)(await this.execute('getDiagnostics', [document], ExecuteMode.Collect, 'high'));
    }
    async doHover(textDocument, position) {
        const document = this.getDocument(textDocument.uri);
        return this.execute('doHover', [document, position], ExecuteMode.FirstNonNull, 'high');
    }
    async getCompletions(textDocument, position, completionContext, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        const completions = await Promise.all(this.plugins.map(async (plugin) => {
            const result = await this.tryExecutePlugin(plugin, 'getCompletions', [document, position, completionContext, cancellationToken], null);
            if (result) {
                return { result: result, plugin: plugin.__name };
            }
        })).then((completions) => completions.filter(utils_1.isNotNullOrUndefined));
        const html = completions.find((completion) => completion.plugin === 'html');
        const ts = completions.find((completion) => completion.plugin === 'ts');
        if (html && ts && (0, documents_1.getNodeIfIsInHTMLStartTag)(document.html, document.offsetAt(position))) {
            // Completion in a component or html start tag and both html and ts
            // suggest something -> filter out all duplicates from TS completions
            const htmlCompletions = new Set(html.result.items.map((item) => item.label));
            ts.result.items = ts.result.items.filter((item) => {
                const label = item.label;
                if (htmlCompletions.has(label)) {
                    return false;
                }
                if (label[0] === '"' && label[label.length - 1] === '"') {
                    // this will result in a wrong completion regardless, remove the quotes
                    item.label = item.label.slice(1, -1);
                    if (htmlCompletions.has(item.label)) {
                        // "aria-label" -> aria-label -> exists in html completions
                        return false;
                    }
                }
                if (label.startsWith('on')) {
                    if (htmlCompletions.has('on:' + label.slice(2))) {
                        // onclick -> on:click -> exists in html completions
                        return false;
                    }
                }
                // adjust sort text so it does appear after html completions
                item.sortText = 'Z' + (item.sortText || '');
                return true;
            });
        }
        let flattenedCompletions = (0, lodash_1.flatten)(completions.map((completion) => completion.result.items));
        const isIncomplete = completions.reduce((incomplete, completion) => incomplete || completion.result.isIncomplete, false);
        // If the result is incomplete, we need to filter the results ourselves
        // to throw out non-matching results. VSCode does filter client-side,
        // but other IDEs might not.
        if (isIncomplete && this.pluginHostConfig.filterIncompleteCompletions) {
            const offset = document.offsetAt(position);
            // Assumption for performance reasons:
            // Noone types import names longer than 20 characters and still expects perfect autocompletion.
            const text = document.getText().substring(Math.max(0, offset - 20), offset);
            const start = (0, utils_1.regexLastIndexOf)(text, /[\W\s]/g) + 1;
            const filterValue = text.substring(start).toLowerCase();
            flattenedCompletions = flattenedCompletions.filter((comp) => comp.label.toLowerCase().includes(filterValue));
        }
        return vscode_languageserver_1.CompletionList.create(flattenedCompletions, isIncomplete);
    }
    async resolveCompletion(textDocument, completionItem, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        const result = await this.execute('resolveCompletion', [document, completionItem, cancellationToken], ExecuteMode.FirstNonNull, 'high');
        return result !== null && result !== void 0 ? result : completionItem;
    }
    async formatDocument(textDocument, options) {
        const document = this.getDocument(textDocument.uri);
        return (0, lodash_1.flatten)(await this.execute('formatDocument', [document, options], ExecuteMode.Collect, 'high'));
    }
    async doTagComplete(textDocument, position) {
        const document = this.getDocument(textDocument.uri);
        return this.execute('doTagComplete', [document, position], ExecuteMode.FirstNonNull, 'high');
    }
    async getDocumentColors(textDocument) {
        const document = this.getDocument(textDocument.uri);
        return (0, lodash_1.flatten)(await this.execute('getDocumentColors', [document], ExecuteMode.Collect, 'low'));
    }
    async getColorPresentations(textDocument, range, color) {
        const document = this.getDocument(textDocument.uri);
        return (0, lodash_1.flatten)(await this.execute('getColorPresentations', [document, range, color], ExecuteMode.Collect, 'high'));
    }
    async getDocumentSymbols(textDocument, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        return (0, lodash_1.flatten)(await this.execute('getDocumentSymbols', [document, cancellationToken], ExecuteMode.Collect, 'low'));
    }
    async getDefinitions(textDocument, position) {
        const document = this.getDocument(textDocument.uri);
        const definitions = (0, lodash_1.flatten)(await this.execute('getDefinitions', [document, position], ExecuteMode.Collect, 'high'));
        if (this.pluginHostConfig.definitionLinkSupport) {
            return definitions;
        }
        else {
            return definitions.map((def) => ({ range: def.targetSelectionRange, uri: def.targetUri }));
        }
    }
    async getCodeActions(textDocument, range, context, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        const actions = (0, lodash_1.flatten)(await this.execute('getCodeActions', [document, range, context, cancellationToken], ExecuteMode.Collect, 'high'));
        // Sort Svelte actions below other actions as they are often less relevant
        actions.sort((a, b) => {
            const aPrio = a.title.startsWith('(svelte)') ? 1 : 0;
            const bPrio = b.title.startsWith('(svelte)') ? 1 : 0;
            return aPrio - bPrio;
        });
        return actions;
    }
    async executeCommand(textDocument, command, args) {
        const document = this.getDocument(textDocument.uri);
        return await this.execute('executeCommand', [document, command, args], ExecuteMode.FirstNonNull, 'high');
    }
    async resolveCodeAction(textDocument, codeAction, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        const result = await this.execute('resolveCodeAction', [document, codeAction, cancellationToken], ExecuteMode.FirstNonNull, 'high');
        return result !== null && result !== void 0 ? result : codeAction;
    }
    async updateImports(fileRename) {
        return await this.execute('updateImports', [fileRename], ExecuteMode.FirstNonNull, 'high');
    }
    async prepareRename(textDocument, position) {
        const document = this.getDocument(textDocument.uri);
        return await this.execute('prepareRename', [document, position], ExecuteMode.FirstNonNull, 'high');
    }
    async rename(textDocument, position, newName) {
        const document = this.getDocument(textDocument.uri);
        return await this.execute('rename', [document, position, newName], ExecuteMode.FirstNonNull, 'high');
    }
    async findReferences(textDocument, position, context) {
        const document = this.getDocument(textDocument.uri);
        return await this.execute('findReferences', [document, position, context], ExecuteMode.FirstNonNull, 'high');
    }
    async fileReferences(uri) {
        return await this.execute('fileReferences', [uri], ExecuteMode.FirstNonNull, 'high');
    }
    async findComponentReferences(uri) {
        return await this.execute('findComponentReferences', [uri], ExecuteMode.FirstNonNull, 'high');
    }
    async getSignatureHelp(textDocument, position, context, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        return await this.execute('getSignatureHelp', [document, position, context, cancellationToken], ExecuteMode.FirstNonNull, 'high');
    }
    /**
     * The selection range supports multiple cursors,
     * each position should return its own selection range tree like `Array.map`.
     * Quote the LSP spec
     * > A selection range in the return array is for the position in the provided parameters at the same index. Therefore positions[i] must be contained in result[i].range.
     * @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_selectionRange
     *
     * Making PluginHost implement the same interface would make it quite hard to get
     * the corresponding selection range of each position from different plugins.
     * Therefore the special treatment here.
     */
    async getSelectionRanges(textDocument, positions) {
        const document = this.getDocument(textDocument.uri);
        try {
            return Promise.all(positions.map(async (position) => {
                var _a;
                for (const plugin of this.plugins) {
                    const range = await ((_a = plugin.getSelectionRange) === null || _a === void 0 ? void 0 : _a.call(plugin, document, position));
                    if (range) {
                        return range;
                    }
                }
                return vscode_languageserver_1.SelectionRange.create(vscode_languageserver_1.Range.create(position, position));
            }));
        }
        catch (error) {
            logger_1.Logger.error(error);
            return null;
        }
    }
    async getSemanticTokens(textDocument, range, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        return await this.execute('getSemanticTokens', [document, range, cancellationToken], ExecuteMode.FirstNonNull, 'smart');
    }
    async getLinkedEditingRanges(textDocument, position) {
        const document = this.getDocument(textDocument.uri);
        return await this.execute('getLinkedEditingRanges', [document, position], ExecuteMode.FirstNonNull, 'high');
    }
    getImplementation(textDocument, position) {
        const document = this.getDocument(textDocument.uri);
        return this.execute('getImplementation', [document, position], ExecuteMode.FirstNonNull, 'high');
    }
    getTypeDefinition(textDocument, position) {
        const document = this.getDocument(textDocument.uri);
        return this.execute('getTypeDefinition', [document, position], ExecuteMode.FirstNonNull, 'high');
    }
    getInlayHints(textDocument, range, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        return this.execute('getInlayHints', [document, range, cancellationToken], ExecuteMode.FirstNonNull, 'smart');
    }
    prepareCallHierarchy(textDocument, position, cancellationToken) {
        const document = this.getDocument(textDocument.uri);
        return this.execute('prepareCallHierarchy', [document, position, cancellationToken], ExecuteMode.FirstNonNull, 'high');
    }
    getIncomingCalls(item, cancellationToken) {
        return this.execute('getIncomingCalls', [item, cancellationToken], ExecuteMode.FirstNonNull, 'high');
    }
    getOutgoingCalls(item, cancellationToken) {
        return this.execute('getOutgoingCalls', [item, cancellationToken], ExecuteMode.FirstNonNull, 'high');
    }
    onWatchFileChanges(onWatchFileChangesParas) {
        var _a;
        for (const support of this.plugins) {
            (_a = support.onWatchFileChanges) === null || _a === void 0 ? void 0 : _a.call(support, onWatchFileChangesParas);
        }
    }
    updateTsOrJsFile(fileName, changes) {
        var _a;
        for (const support of this.plugins) {
            (_a = support.updateTsOrJsFile) === null || _a === void 0 ? void 0 : _a.call(support, fileName, changes);
        }
    }
    getDocument(uri) {
        const document = this.documentsManager.get(uri);
        if (!document) {
            throw new Error('Cannot call methods on an unopened document');
        }
        return document;
    }
    async execute(name, args, mode, priority) {
        var _a;
        const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function');
        // Priority 'smart' tries to aproximate how much time a method takes to execute,
        // making it low priority if it takes too long or if it seems like other methods do.
        const now = perf_hooks_1.performance.now();
        if (priority === 'smart' &&
            (((_a = this.requestTimings[name]) === null || _a === void 0 ? void 0 : _a[0]) > 500 ||
                Object.values(this.requestTimings).filter((t) => t[0] > 400 && now - t[1] < 60 * 1000).length > 2)) {
            logger_1.Logger.debug(`Executing next invocation of "${name}" with low priority`);
            priority = 'low';
            if (this.requestTimings[name]) {
                this.requestTimings[name][0] = this.requestTimings[name][0] / 2 + 150;
            }
        }
        if (priority === 'low') {
            // If a request doesn't have priority, we first wait 1 second to
            // 1. let higher priority requests get through first
            // 2. wait for possible document changes, which make the request wait again
            // Due to waiting, low priority items should preferrably be those who do not
            // rely on positions or ranges and rather on the whole document only.
            const debounce = async () => {
                const id = Math.random();
                this.deferredRequests[name] = [
                    id,
                    new Promise((resolve, reject) => {
                        setTimeout(() => {
                            if (!this.deferredRequests[name] ||
                                this.deferredRequests[name][0] === id) {
                                resolve();
                            }
                            else {
                                // We should not get into this case. According to the spec,
                                // the language client does not send another request
                                // of the same type until the previous one is answered.
                                reject();
                            }
                        }, 1000);
                    })
                ];
                try {
                    await this.deferredRequests[name][1];
                    if (!this.deferredRequests[name]) {
                        return debounce();
                    }
                    return true;
                }
                catch (e) {
                    return false;
                }
            };
            const shouldContinue = await debounce();
            if (!shouldContinue) {
                return;
            }
        }
        const startTime = perf_hooks_1.performance.now();
        const result = await this.executePlugins(name, args, mode, plugins);
        this.requestTimings[name] = [perf_hooks_1.performance.now() - startTime, startTime];
        return result;
    }
    async executePlugins(name, args, mode, plugins) {
        switch (mode) {
            case ExecuteMode.FirstNonNull:
                for (const plugin of plugins) {
                    const res = await this.tryExecutePlugin(plugin, name, args, null);
                    if (res != null) {
                        return res;
                    }
                }
                return null;
            case ExecuteMode.Collect:
                return Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, [])));
            case ExecuteMode.None:
                await Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null)));
                return;
        }
    }
    async tryExecutePlugin(plugin, fnName, args, failValue) {
        try {
            return await plugin[fnName](...args);
        }
        catch (e) {
            logger_1.Logger.error(e);
            return failValue;
        }
    }
}
exports.PluginHost = PluginHost;
//# sourceMappingURL=PluginHost.js.map