"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const TurndownService = require("turndown");
const LSP = require("vscode-languageserver");
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
const analyser_1 = require("./analyser");
const Builtins = require("./builtins");
const config = require("./config");
const executables_1 = require("./executables");
const parser_1 = require("./parser");
const ReservedWords = require("./reservedWords");
const types_1 = require("./types");
const array_1 = require("./util/array");
const sh_1 = require("./util/sh");
const PARAMETER_EXPANSION_PREFIXES = new Set(['$', '${']);
/**
 * The BashServer glues together the separate components to implement
 * the various parts of the Language Server Protocol.
 */
class BashServer {
    constructor(connection, executables, analyzer) {
        this.documents = new LSP.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
        this.connection = connection;
        this.executables = executables;
        this.analyzer = analyzer;
    }
    /**
     * Initialize the server based on a connection to the client and the protocols
     * initialization parameters.
     */
    static initialize(connection, { rootPath }) {
        return __awaiter(this, void 0, void 0, function* () {
            const parser = yield parser_1.initializeParser();
            const { PATH } = process.env;
            if (!PATH) {
                throw new Error('Expected PATH environment variable to be set');
            }
            return Promise.all([
                executables_1.default.fromPath(PATH),
                analyser_1.default.fromRoot({ connection, rootPath, parser }),
            ]).then(xs => {
                const executables = xs[0];
                const analyzer = xs[1];
                return new BashServer(connection, executables, analyzer);
            });
        });
    }
    /**
     * Register handlers for the events from the Language Server Protocol that we
     * care about.
     */
    register(connection) {
        // The content of a text document has changed. This event is emitted
        // when the text document first opened or when its content has changed.
        this.documents.listen(this.connection);
        this.documents.onDidChangeContent(change => {
            const { uri } = change.document;
            const diagnostics = this.analyzer.analyze(uri, change.document);
            if (config.getHighlightParsingError()) {
                connection.sendDiagnostics({
                    uri: change.document.uri,
                    diagnostics,
                });
            }
        });
        // Register all the handlers for the LSP events.
        connection.onHover(this.onHover.bind(this));
        connection.onDefinition(this.onDefinition.bind(this));
        connection.onDocumentSymbol(this.onDocumentSymbol.bind(this));
        connection.onWorkspaceSymbol(this.onWorkspaceSymbol.bind(this));
        connection.onDocumentHighlight(this.onDocumentHighlight.bind(this));
        connection.onReferences(this.onReferences.bind(this));
        connection.onCompletion(this.onCompletion.bind(this));
        connection.onCompletionResolve(this.onCompletionResolve.bind(this));
    }
    /**
     * The parts of the Language Server Protocol that we are currently supporting.
     */
    capabilities() {
        return {
            // For now we're using full-sync even though tree-sitter has great support
            // for partial updates.
            textDocumentSync: LSP.TextDocumentSyncKind.Full,
            completionProvider: {
                resolveProvider: true,
                triggerCharacters: ['$', '{'],
            },
            hoverProvider: true,
            documentHighlightProvider: true,
            definitionProvider: true,
            documentSymbolProvider: true,
            workspaceSymbolProvider: true,
            referencesProvider: true,
        };
    }
    getWordAtPoint(params) {
        return this.analyzer.wordAtPoint(params.textDocument.uri, params.position.line, params.position.character);
    }
    logRequest({ request, params, word, }) {
        const wordLog = word ? `"${word}"` : 'null';
        this.connection.console.log(`${request} ${params.position.line}:${params.position.character} word=${wordLog}`);
    }
    getDocumentationForSymbol({ currentUri, symbol, }) {
        const symbolUri = symbol.location.uri;
        const symbolStarLine = symbol.location.range.start.line;
        const commentAboveSymbol = this.analyzer.commentsAbove(symbolUri, symbolStarLine);
        const symbolDocumentation = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : '';
        return symbolUri !== currentUri
            ? `${symbolKindToDescription(symbol.kind)} defined in ${path.relative(currentUri, symbolUri)}${symbolDocumentation}`
            : `${symbolKindToDescription(symbol.kind)} defined on line ${symbolStarLine +
                1}${symbolDocumentation}`;
    }
    getCompletionItemsForSymbols({ symbols, currentUri, }) {
        return deduplicateSymbols({ symbols, currentUri }).map((symbol) => ({
            label: symbol.name,
            kind: symbolKindToCompletionKind(symbol.kind),
            data: {
                name: symbol.name,
                type: types_1.CompletionItemDataType.Symbol,
            },
            documentation: symbol.location.uri !== currentUri
                ? this.getDocumentationForSymbol({
                    currentUri,
                    symbol,
                })
                : undefined,
        }));
    }
    onHover(params) {
        return __awaiter(this, void 0, void 0, function* () {
            const word = this.getWordAtPoint(params);
            const currentUri = params.textDocument.uri;
            this.logRequest({ request: 'onHover', params, word });
            if (!word || word.startsWith('#')) {
                return null;
            }
            const explainshellEndpoint = config.getExplainshellEndpoint();
            if (explainshellEndpoint) {
                this.connection.console.log(`Query ${explainshellEndpoint}`);
                try {
                    const response = yield this.analyzer.getExplainshellDocumentation({
                        params,
                        endpoint: explainshellEndpoint,
                    });
                    if (response.status === 'error') {
                        this.connection.console.log(`getExplainshellDocumentation returned: ${JSON.stringify(response, null, 4)}`);
                    }
                    else {
                        return {
                            contents: {
                                kind: 'markdown',
                                value: new TurndownService().turndown(response.helpHTML),
                            },
                        };
                    }
                }
                catch (error) {
                    this.connection.console.warn(`getExplainshellDocumentation exception: ${error.message}`);
                }
            }
            if (ReservedWords.isReservedWord(word) ||
                Builtins.isBuiltin(word) ||
                this.executables.isExecutableOnPATH(word)) {
                const shellDocumentation = yield sh_1.getShellDocumentation({ word });
                if (shellDocumentation) {
                    // eslint-disable-next-line no-console
                    return { contents: getMarkdownContent(shellDocumentation) };
                }
            }
            else {
                const symbolDocumentation = deduplicateSymbols({
                    symbols: this.analyzer.findSymbolsMatchingWord({
                        exactMatch: true,
                        word,
                    }),
                    currentUri,
                })
                    // do not return hover referencing for the current line
                    .filter(symbol => symbol.location.range.start.line !== params.position.line)
                    .map((symbol) => this.getDocumentationForSymbol({ currentUri, symbol }));
                if (symbolDocumentation.length === 1) {
                    return { contents: symbolDocumentation[0] };
                }
            }
            return null;
        });
    }
    onDefinition(params) {
        const word = this.getWordAtPoint(params);
        this.logRequest({ request: 'onDefinition', params, word });
        if (!word) {
            return null;
        }
        return this.analyzer.findDefinition(word);
    }
    onDocumentSymbol(params) {
        this.connection.console.log(`onDocumentSymbol`);
        return this.analyzer.findSymbolsForFile({ uri: params.textDocument.uri });
    }
    onWorkspaceSymbol(params) {
        this.connection.console.log('onWorkspaceSymbol');
        return this.analyzer.search(params.query);
    }
    onDocumentHighlight(params) {
        const word = this.getWordAtPoint(params);
        this.logRequest({ request: 'onDocumentHighlight', params, word });
        if (!word) {
            return [];
        }
        return this.analyzer
            .findOccurrences(params.textDocument.uri, word)
            .map(n => ({ range: n.range }));
    }
    onReferences(params) {
        const word = this.getWordAtPoint(params);
        this.logRequest({ request: 'onReferences', params, word });
        if (!word) {
            return null;
        }
        return this.analyzer.findReferences(word);
    }
    onCompletion(params) {
        const word = this.getWordAtPoint(Object.assign(Object.assign({}, params), { position: {
                line: params.position.line,
                // Go one character back to get completion on the current word
                character: Math.max(params.position.character - 1, 0),
            } }));
        this.logRequest({ request: 'onCompletion', params, word });
        if (word && word.startsWith('#')) {
            // Inside a comment block
            return [];
        }
        if (word && word === '{') {
            // We should not complete when it is not prefixed by a $.
            // This case needs to be here
            // because { is a completionProvider triggerCharacter.
            return [];
        }
        const currentUri = params.textDocument.uri;
        // TODO: an improvement here would be to detect if the current word is
        // not only a parameter expansion prefix, but also if the word is actually
        // inside a parameter expansion (e.g. auto completing on a word $MY_VARIA).
        const shouldCompleteOnVariables = word
            ? PARAMETER_EXPANSION_PREFIXES.has(word)
            : false;
        const symbolCompletions = word === null
            ? []
            : this.getCompletionItemsForSymbols({
                symbols: shouldCompleteOnVariables
                    ? this.analyzer.getAllVariableSymbols()
                    : this.analyzer.findSymbolsMatchingWord({
                        exactMatch: false,
                        word,
                    }),
                currentUri,
            });
        if (shouldCompleteOnVariables) {
            // In case we auto complete on a word that starts a parameter expansion,
            // we do not return anything else than variable/parameter suggestions.
            // Note: that LSP clients should not call onCompletion in the middle
            // of a word, so the following should work for client.
            return symbolCompletions;
        }
        const reservedWordsCompletions = ReservedWords.LIST.map(reservedWord => ({
            label: reservedWord,
            kind: LSP.SymbolKind.Interface,
            data: {
                name: reservedWord,
                type: types_1.CompletionItemDataType.ReservedWord,
            },
        }));
        const programCompletions = this.executables
            .list()
            .filter(executable => !Builtins.isBuiltin(executable))
            .map(executable => {
            return {
                label: executable,
                kind: LSP.SymbolKind.Function,
                data: {
                    name: executable,
                    type: types_1.CompletionItemDataType.Executable,
                },
            };
        });
        const builtinsCompletions = Builtins.LIST.map(builtin => ({
            label: builtin,
            kind: LSP.SymbolKind.Interface,
            data: {
                name: builtin,
                type: types_1.CompletionItemDataType.Builtin,
            },
        }));
        const allCompletions = [
            ...reservedWordsCompletions,
            ...symbolCompletions,
            ...programCompletions,
            ...builtinsCompletions,
        ];
        if (word) {
            // Filter to only return suffixes of the current word
            return allCompletions.filter(item => item.label.startsWith(word));
        }
        return allCompletions;
    }
    onCompletionResolve(item) {
        return __awaiter(this, void 0, void 0, function* () {
            const { data: { name, type }, } = item;
            this.connection.console.log(`onCompletionResolve name=${name} type=${type}`);
            try {
                let documentation = null;
                if (type === types_1.CompletionItemDataType.Executable ||
                    type === types_1.CompletionItemDataType.Builtin ||
                    type === types_1.CompletionItemDataType.ReservedWord) {
                    documentation = yield sh_1.getShellDocumentation({ word: name });
                }
                return documentation
                    ? Object.assign(Object.assign({}, item), { documentation: getMarkdownContent(documentation) }) : item;
            }
            catch (error) {
                return item;
            }
        });
    }
}
exports.default = BashServer;
/**
 * Deduplicate symbols by prioritizing the current file.
 */
function deduplicateSymbols({ symbols, currentUri, }) {
    const isCurrentFile = ({ location: { uri } }) => uri === currentUri;
    const getSymbolId = ({ name, kind }) => `${name}${kind}`;
    const symbolsCurrentFile = symbols.filter(s => isCurrentFile(s));
    const symbolsOtherFiles = symbols
        .filter(s => !isCurrentFile(s))
        // Remove identical symbols matching current file
        .filter(symbolOtherFiles => !symbolsCurrentFile.some(symbolCurrentFile => getSymbolId(symbolCurrentFile) === getSymbolId(symbolOtherFiles)));
    return array_1.uniqueBasedOnHash([...symbolsCurrentFile, ...symbolsOtherFiles], getSymbolId);
}
function symbolKindToCompletionKind(s) {
    switch (s) {
        case LSP.SymbolKind.File:
            return LSP.CompletionItemKind.File;
        case LSP.SymbolKind.Module:
        case LSP.SymbolKind.Namespace:
        case LSP.SymbolKind.Package:
            return LSP.CompletionItemKind.Module;
        case LSP.SymbolKind.Class:
            return LSP.CompletionItemKind.Class;
        case LSP.SymbolKind.Method:
            return LSP.CompletionItemKind.Method;
        case LSP.SymbolKind.Property:
            return LSP.CompletionItemKind.Property;
        case LSP.SymbolKind.Field:
            return LSP.CompletionItemKind.Field;
        case LSP.SymbolKind.Constructor:
            return LSP.CompletionItemKind.Constructor;
        case LSP.SymbolKind.Enum:
            return LSP.CompletionItemKind.Enum;
        case LSP.SymbolKind.Interface:
            return LSP.CompletionItemKind.Interface;
        case LSP.SymbolKind.Function:
            return LSP.CompletionItemKind.Function;
        case LSP.SymbolKind.Variable:
            return LSP.CompletionItemKind.Variable;
        case LSP.SymbolKind.Constant:
            return LSP.CompletionItemKind.Constant;
        case LSP.SymbolKind.String:
        case LSP.SymbolKind.Number:
        case LSP.SymbolKind.Boolean:
        case LSP.SymbolKind.Array:
        case LSP.SymbolKind.Key:
        case LSP.SymbolKind.Null:
            return LSP.CompletionItemKind.Text;
        case LSP.SymbolKind.Object:
            return LSP.CompletionItemKind.Module;
        case LSP.SymbolKind.EnumMember:
            return LSP.CompletionItemKind.EnumMember;
        case LSP.SymbolKind.Struct:
            return LSP.CompletionItemKind.Struct;
        case LSP.SymbolKind.Event:
            return LSP.CompletionItemKind.Event;
        case LSP.SymbolKind.Operator:
            return LSP.CompletionItemKind.Operator;
        case LSP.SymbolKind.TypeParameter:
            return LSP.CompletionItemKind.TypeParameter;
        default:
            return LSP.CompletionItemKind.Text;
    }
}
function symbolKindToDescription(s) {
    switch (s) {
        case LSP.SymbolKind.Function:
            return 'Function';
        case LSP.SymbolKind.Variable:
            return 'Variable';
        default:
            return 'Keyword';
    }
}
const getMarkdownContent = (documentation) => ({
    value: ['``` man', documentation, '```'].join('\n'),
    // Passed as markdown for syntax highlighting
    kind: 'markdown',
});
//# sourceMappingURL=server.js.map