"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");
/**
 * 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,
            },
            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}`);
    }
    onHover(params) {
        return __awaiter(this, void 0, void 0, function* () {
            const word = this.getWordAtPoint(params);
            this.logRequest({ request: 'onHover', params, word });
            if (!word) {
                return null;
            }
            const explainshellEndpoint = config.getExplainshellEndpoint();
            if (explainshellEndpoint) {
                this.connection.console.log(`Query ${explainshellEndpoint}`);
                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),
                        },
                    };
                }
            }
            const getMarkdownHoverItem = (doc) => ({
                // LSP.MarkupContent
                value: ['``` man', doc, '```'].join('\n'),
                // Passed as markdown for syntax highlighting
                kind: 'markdown',
            });
            if (Builtins.isBuiltin(word)) {
                return Builtins.documentation(word).then(doc => ({
                    contents: getMarkdownHoverItem(doc),
                }));
            }
            if (ReservedWords.isReservedWord(word)) {
                return ReservedWords.documentation(word).then(doc => ({
                    contents: getMarkdownHoverItem(doc),
                }));
            }
            if (this.executables.isExecutableOnPATH(word)) {
                return this.executables.documentation(word).then(doc => ({
                    contents: getMarkdownHoverItem(doc),
                }));
            }
            // FIXME: could also be a symbol
            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 null;
        }
        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(params);
        this.logRequest({ request: 'onCompletion', params, word });
        const currentUri = params.textDocument.uri;
        const symbolCompletions = word === null
            ? []
            : getCompletionItemsForSymbols({
                symbols: this.analyzer.findSymbolsMatchingWord({
                    word,
                }),
                currentUri,
            });
        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) {
            if (word.startsWith('#')) {
                // Inside a comment block
                return [];
            }
            // 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}`);
            const getMarkdownCompletionItem = (doc) => (Object.assign(Object.assign({}, item), { 
                // LSP.MarkupContent
                documentation: {
                    value: ['``` man', doc, '```'].join('\n'),
                    // Passed as markdown for syntax highlighting
                    kind: 'markdown',
                } }));
            try {
                if (type === types_1.CompletionItemDataType.Executable) {
                    const doc = yield this.executables.documentation(name);
                    return getMarkdownCompletionItem(doc);
                }
                else if (type === types_1.CompletionItemDataType.Builtin) {
                    const doc = yield Builtins.documentation(name);
                    return getMarkdownCompletionItem(doc);
                }
                else if (type === types_1.CompletionItemDataType.ReservedWord) {
                    const doc = yield ReservedWords.documentation(name);
                    return getMarkdownCompletionItem(doc);
                }
                else {
                    return item;
                }
            }
            catch (error) {
                return item;
            }
        });
    }
}
exports.default = BashServer;
function getCompletionItemsForSymbols({ 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
        .filter(symbolOtherFiles => !symbolsCurrentFile.some(symbolCurrentFile => getSymbolId(symbolCurrentFile) === getSymbolId(symbolOtherFiles)));
    return array_1.uniqueBasedOnHash([...symbolsCurrentFile, ...symbolsOtherFiles], getSymbolId).map((symbol) => ({
        label: symbol.name,
        kind: symbolKindToCompletionKind(symbol.kind),
        data: {
            name: symbol.name,
            type: types_1.CompletionItemDataType.Symbol,
        },
        documentation: symbol.location.uri !== currentUri
            ? `${symbolKindToDescription(symbol.kind)} defined in ${path.relative(currentUri, symbol.location.uri)}`
            : undefined,
    }));
}
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';
    }
}
//# sourceMappingURL=server.js.map