"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 });
exports.getJavaScriptMode = getJavaScriptMode;
const languageModelCache_1 = require("../languageModelCache");
const languageModes_1 = require("./languageModes");
const strings_1 = require("../utils/strings");
const ts = require("typescript");
const javascriptSemanticTokens_1 = require("./javascriptSemanticTokens");
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
function getLanguageServiceHost(scriptKind) {
    const compilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es2020.full.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false };
    let currentTextDocument = languageModes_1.TextDocument.create('init', 'javascript', 1, '');
    const jsLanguageService = Promise.resolve().then(() => require('./javascriptLibs')).then(libs => {
        const host = {
            getCompilationSettings: () => compilerOptions,
            getScriptFileNames: () => [currentTextDocument.uri, 'jquery'],
            getScriptKind: (fileName) => {
                if (fileName === currentTextDocument.uri) {
                    return scriptKind;
                }
                return fileName.substr(fileName.length - 2) === 'ts' ? ts.ScriptKind.TS : ts.ScriptKind.JS;
            },
            getScriptVersion: (fileName) => {
                if (fileName === currentTextDocument.uri) {
                    return String(currentTextDocument.version);
                }
                return '1';
            },
            getScriptSnapshot: (fileName) => {
                let text = '';
                if (fileName === currentTextDocument.uri) {
                    text = currentTextDocument.getText();
                }
                else {
                    text = libs.loadLibrary(fileName);
                }
                return {
                    getText: (start, end) => text.substring(start, end),
                    getLength: () => text.length,
                    getChangeRange: () => undefined
                };
            },
            getCurrentDirectory: () => '',
            getDefaultLibFileName: (_options) => 'es2020.full',
            readFile: (path, _encoding) => {
                if (path === currentTextDocument.uri) {
                    return currentTextDocument.getText();
                }
                else {
                    return libs.loadLibrary(path);
                }
            },
            fileExists: (path) => {
                if (path === currentTextDocument.uri) {
                    return true;
                }
                else {
                    return !!libs.loadLibrary(path);
                }
            },
            directoryExists: (path) => {
                if (path.startsWith('node_modules')) {
                    return false;
                }
                return true;
            }
        };
        return ts.createLanguageService(host);
    });
    return {
        getLanguageService(jsDocument) {
            return __awaiter(this, void 0, void 0, function* () {
                currentTextDocument = jsDocument;
                return jsLanguageService;
            });
        },
        getCompilationSettings() {
            return compilerOptions;
        },
        dispose() {
            jsLanguageService.then(s => s.dispose());
        }
    };
}
const ignoredErrors = [
    1108,
    2792,
];
function getJavaScriptMode(documentRegions, languageId, workspace) {
    const jsDocuments = (0, languageModelCache_1.getLanguageModelCache)(10, 60, document => documentRegions.get(document).getEmbeddedDocument(languageId));
    const host = getLanguageServiceHost(languageId === 'javascript' ? ts.ScriptKind.JS : ts.ScriptKind.TS);
    const globalSettings = {};
    function updateHostSettings(settings) {
        var _a, _b, _c;
        const hostSettings = host.getCompilationSettings();
        hostSettings.experimentalDecorators = (_b = (_a = settings === null || settings === void 0 ? void 0 : settings['js/ts']) === null || _a === void 0 ? void 0 : _a.implicitProjectConfig) === null || _b === void 0 ? void 0 : _b.experimentalDecorators;
        hostSettings.strictNullChecks = (_c = settings === null || settings === void 0 ? void 0 : settings['js/ts']) === null || _c === void 0 ? void 0 : _c.implicitProjectConfig.strictNullChecks;
    }
    return {
        getId() {
            return languageId;
        },
        doValidation(document_1) {
            return __awaiter(this, arguments, void 0, function* (document, settings = workspace.settings) {
                updateHostSettings(settings);
                const jsDocument = jsDocuments.get(document);
                const languageService = yield host.getLanguageService(jsDocument);
                const syntaxDiagnostics = languageService.getSyntacticDiagnostics(jsDocument.uri);
                const semanticDiagnostics = languageService.getSemanticDiagnostics(jsDocument.uri);
                return syntaxDiagnostics.concat(semanticDiagnostics).filter(d => !ignoredErrors.includes(d.code)).map((diag) => {
                    return {
                        range: convertRange(jsDocument, diag),
                        severity: languageModes_1.DiagnosticSeverity.Error,
                        source: languageId,
                        message: ts.flattenDiagnosticMessageText(diag.messageText, '\n')
                    };
                });
            });
        },
        doComplete(document, position, _documentContext) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const offset = jsDocument.offsetAt(position);
                const completions = jsLanguageService.getCompletionsAtPosition(jsDocument.uri, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
                if (!completions) {
                    return { isIncomplete: false, items: [] };
                }
                const replaceRange = convertRange(jsDocument, (0, strings_1.getWordAtText)(jsDocument.getText(), offset, JS_WORD_REGEX));
                return {
                    isIncomplete: false,
                    items: completions.entries.map(entry => {
                        const data = {
                            languageId,
                            uri: document.uri,
                            offset: offset
                        };
                        return {
                            uri: document.uri,
                            position: position,
                            label: entry.name,
                            sortText: entry.sortText,
                            kind: convertKind(entry.kind),
                            textEdit: languageModes_1.TextEdit.replace(replaceRange, entry.name),
                            data
                        };
                    })
                };
            });
        },
        doResolve(document, item) {
            return __awaiter(this, void 0, void 0, function* () {
                if ((0, languageModes_1.isCompletionItemData)(item.data)) {
                    const jsDocument = jsDocuments.get(document);
                    const jsLanguageService = yield host.getLanguageService(jsDocument);
                    const details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined, undefined);
                    if (details) {
                        item.detail = ts.displayPartsToString(details.displayParts);
                        item.documentation = ts.displayPartsToString(details.documentation);
                        delete item.data;
                    }
                }
                return item;
            });
        },
        doHover(document, position) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const info = jsLanguageService.getQuickInfoAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
                if (info) {
                    const contents = ts.displayPartsToString(info.displayParts);
                    return {
                        range: convertRange(jsDocument, info.textSpan),
                        contents: ['```typescript', contents, '```'].join('\n')
                    };
                }
                return null;
            });
        },
        doSignatureHelp(document, position) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const signHelp = jsLanguageService.getSignatureHelpItems(jsDocument.uri, jsDocument.offsetAt(position), undefined);
                if (signHelp) {
                    const ret = {
                        activeSignature: signHelp.selectedItemIndex,
                        activeParameter: signHelp.argumentIndex,
                        signatures: []
                    };
                    signHelp.items.forEach(item => {
                        const signature = {
                            label: '',
                            documentation: undefined,
                            parameters: []
                        };
                        signature.label += ts.displayPartsToString(item.prefixDisplayParts);
                        item.parameters.forEach((p, i, a) => {
                            const label = ts.displayPartsToString(p.displayParts);
                            const parameter = {
                                label: label,
                                documentation: ts.displayPartsToString(p.documentation)
                            };
                            signature.label += label;
                            signature.parameters.push(parameter);
                            if (i < a.length - 1) {
                                signature.label += ts.displayPartsToString(item.separatorDisplayParts);
                            }
                        });
                        signature.label += ts.displayPartsToString(item.suffixDisplayParts);
                        ret.signatures.push(signature);
                    });
                    return ret;
                }
                return null;
            });
        },
        doRename(document, position, newName) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const jsDocumentPosition = jsDocument.offsetAt(position);
                const { canRename } = jsLanguageService.getRenameInfo(jsDocument.uri, jsDocumentPosition);
                if (!canRename) {
                    return null;
                }
                const renameInfos = jsLanguageService.findRenameLocations(jsDocument.uri, jsDocumentPosition, false, false);
                const edits = [];
                renameInfos === null || renameInfos === void 0 ? void 0 : renameInfos.map(renameInfo => {
                    edits.push({
                        range: convertRange(jsDocument, renameInfo.textSpan),
                        newText: newName,
                    });
                });
                return {
                    changes: { [document.uri]: edits },
                };
            });
        },
        findDocumentHighlight(document, position) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const highlights = jsLanguageService.getDocumentHighlights(jsDocument.uri, jsDocument.offsetAt(position), [jsDocument.uri]);
                const out = [];
                for (const entry of highlights || []) {
                    for (const highlight of entry.highlightSpans) {
                        out.push({
                            range: convertRange(jsDocument, highlight.textSpan),
                            kind: highlight.kind === 'writtenReference' ? languageModes_1.DocumentHighlightKind.Write : languageModes_1.DocumentHighlightKind.Text
                        });
                    }
                }
                return out;
            });
        },
        findDocumentSymbols(document) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const items = jsLanguageService.getNavigationBarItems(jsDocument.uri);
                if (items) {
                    const result = [];
                    const existing = Object.create(null);
                    const collectSymbols = (item, containerLabel) => {
                        const sig = item.text + item.kind + item.spans[0].start;
                        if (item.kind !== 'script' && !existing[sig]) {
                            const symbol = {
                                name: item.text,
                                kind: convertSymbolKind(item.kind),
                                location: {
                                    uri: document.uri,
                                    range: convertRange(jsDocument, item.spans[0])
                                },
                                containerName: containerLabel
                            };
                            existing[sig] = true;
                            result.push(symbol);
                            containerLabel = item.text;
                        }
                        if (item.childItems && item.childItems.length > 0) {
                            for (const child of item.childItems) {
                                collectSymbols(child, containerLabel);
                            }
                        }
                    };
                    items.forEach(item => collectSymbols(item));
                    return result;
                }
                return [];
            });
        },
        findDefinition(document, position) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const definition = jsLanguageService.getDefinitionAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
                if (definition) {
                    return definition.filter(d => d.fileName === jsDocument.uri).map(d => {
                        return {
                            uri: document.uri,
                            range: convertRange(jsDocument, d.textSpan)
                        };
                    });
                }
                return null;
            });
        },
        findReferences(document, position) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const references = jsLanguageService.getReferencesAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
                if (references) {
                    return references.filter(d => d.fileName === jsDocument.uri).map(d => {
                        return {
                            uri: document.uri,
                            range: convertRange(jsDocument, d.textSpan)
                        };
                    });
                }
                return [];
            });
        },
        getSelectionRange(document, position) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                function convertSelectionRange(selectionRange) {
                    const parent = selectionRange.parent ? convertSelectionRange(selectionRange.parent) : undefined;
                    return languageModes_1.SelectionRange.create(convertRange(jsDocument, selectionRange.textSpan), parent);
                }
                const range = jsLanguageService.getSmartSelectionRange(jsDocument.uri, jsDocument.offsetAt(position));
                return convertSelectionRange(range);
            });
        },
        format(document_1, range_1, formatParams_1) {
            return __awaiter(this, arguments, void 0, function* (document, range, formatParams, settings = globalSettings) {
                const jsDocument = documentRegions.get(document).getEmbeddedDocument('javascript', true);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const formatterSettings = settings && settings.javascript && settings.javascript.format;
                const initialIndentLevel = computeInitialIndent(document, range, formatParams);
                const formatSettings = convertOptions(formatParams, formatterSettings, initialIndentLevel + 1);
                const start = jsDocument.offsetAt(range.start);
                let end = jsDocument.offsetAt(range.end);
                let lastLineRange = null;
                if (range.end.line > range.start.line && (range.end.character === 0 || (0, strings_1.isWhitespaceOnly)(jsDocument.getText().substr(end - range.end.character, range.end.character)))) {
                    end -= range.end.character;
                    lastLineRange = languageModes_1.Range.create(languageModes_1.Position.create(range.end.line, 0), range.end);
                }
                const edits = jsLanguageService.getFormattingEditsForRange(jsDocument.uri, start, end, formatSettings);
                if (edits) {
                    const result = [];
                    for (const edit of edits) {
                        if (edit.span.start >= start && edit.span.start + edit.span.length <= end) {
                            result.push({
                                range: convertRange(jsDocument, edit.span),
                                newText: edit.newText
                            });
                        }
                    }
                    if (lastLineRange) {
                        result.push({
                            range: lastLineRange,
                            newText: generateIndent(initialIndentLevel, formatParams)
                        });
                    }
                    return result;
                }
                return [];
            });
        },
        getFoldingRanges(document) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                const spans = jsLanguageService.getOutliningSpans(jsDocument.uri);
                const ranges = [];
                for (const span of spans) {
                    const curr = convertRange(jsDocument, span.textSpan);
                    const startLine = curr.start.line;
                    const endLine = curr.end.line;
                    if (startLine < endLine) {
                        const foldingRange = { startLine, endLine };
                        const match = document.getText(curr).match(/^\s*\/(?:(\/\s*#(?:end)?region\b)|(\*|\/))/);
                        if (match) {
                            foldingRange.kind = match[1] ? languageModes_1.FoldingRangeKind.Region : languageModes_1.FoldingRangeKind.Comment;
                        }
                        ranges.push(foldingRange);
                    }
                }
                return ranges;
            });
        },
        onDocumentRemoved(document) {
            jsDocuments.onDocumentRemoved(document);
        },
        getSemanticTokens(document) {
            return __awaiter(this, void 0, void 0, function* () {
                const jsDocument = jsDocuments.get(document);
                const jsLanguageService = yield host.getLanguageService(jsDocument);
                return [...(0, javascriptSemanticTokens_1.getSemanticTokens)(jsLanguageService, jsDocument, jsDocument.uri)];
            });
        },
        getSemanticTokenLegend() {
            return (0, javascriptSemanticTokens_1.getSemanticTokenLegend)();
        },
        dispose() {
            host.dispose();
            jsDocuments.dispose();
        }
    };
}
function convertRange(document, span) {
    if (typeof span.start === 'undefined') {
        const pos = document.positionAt(0);
        return languageModes_1.Range.create(pos, pos);
    }
    const startPosition = document.positionAt(span.start);
    const endPosition = document.positionAt(span.start + (span.length || 0));
    return languageModes_1.Range.create(startPosition, endPosition);
}
function convertKind(kind) {
    switch (kind) {
        case "primitive type":
        case "keyword":
            return languageModes_1.CompletionItemKind.Keyword;
        case "const":
        case "let":
        case "var":
        case "local var":
        case "alias":
        case "parameter":
            return languageModes_1.CompletionItemKind.Variable;
        case "property":
        case "getter":
        case "setter":
            return languageModes_1.CompletionItemKind.Field;
        case "function":
        case "local function":
            return languageModes_1.CompletionItemKind.Function;
        case "method":
        case "construct":
        case "call":
        case "index":
            return languageModes_1.CompletionItemKind.Method;
        case "enum":
            return languageModes_1.CompletionItemKind.Enum;
        case "enum member":
            return languageModes_1.CompletionItemKind.EnumMember;
        case "module":
        case "external module name":
            return languageModes_1.CompletionItemKind.Module;
        case "class":
        case "type":
            return languageModes_1.CompletionItemKind.Class;
        case "interface":
            return languageModes_1.CompletionItemKind.Interface;
        case "warning":
            return languageModes_1.CompletionItemKind.Text;
        case "script":
            return languageModes_1.CompletionItemKind.File;
        case "directory":
            return languageModes_1.CompletionItemKind.Folder;
        case "string":
            return languageModes_1.CompletionItemKind.Constant;
        default:
            return languageModes_1.CompletionItemKind.Property;
    }
}
function convertSymbolKind(kind) {
    switch (kind) {
        case "module": return languageModes_1.SymbolKind.Module;
        case "class": return languageModes_1.SymbolKind.Class;
        case "enum": return languageModes_1.SymbolKind.Enum;
        case "enum member": return languageModes_1.SymbolKind.EnumMember;
        case "interface": return languageModes_1.SymbolKind.Interface;
        case "index": return languageModes_1.SymbolKind.Method;
        case "call": return languageModes_1.SymbolKind.Method;
        case "method": return languageModes_1.SymbolKind.Method;
        case "property": return languageModes_1.SymbolKind.Property;
        case "getter": return languageModes_1.SymbolKind.Property;
        case "setter": return languageModes_1.SymbolKind.Property;
        case "var": return languageModes_1.SymbolKind.Variable;
        case "let": return languageModes_1.SymbolKind.Variable;
        case "const": return languageModes_1.SymbolKind.Variable;
        case "local var": return languageModes_1.SymbolKind.Variable;
        case "alias": return languageModes_1.SymbolKind.Variable;
        case "function": return languageModes_1.SymbolKind.Function;
        case "local function": return languageModes_1.SymbolKind.Function;
        case "construct": return languageModes_1.SymbolKind.Constructor;
        case "constructor": return languageModes_1.SymbolKind.Constructor;
        case "type parameter": return languageModes_1.SymbolKind.TypeParameter;
        case "string": return languageModes_1.SymbolKind.String;
        default: return languageModes_1.SymbolKind.Variable;
    }
}
function convertOptions(options, formatSettings, initialIndentLevel) {
    return {
        convertTabsToSpaces: options.insertSpaces,
        tabSize: options.tabSize,
        indentSize: options.tabSize,
        indentStyle: ts.IndentStyle.Smart,
        newLineCharacter: '\n',
        baseIndentSize: options.tabSize * initialIndentLevel,
        insertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter),
        insertSpaceAfterConstructor: Boolean(formatSettings && formatSettings.insertSpaceAfterConstructor),
        insertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements),
        insertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators),
        insertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements),
        insertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions),
        insertSpaceBeforeFunctionParenthesis: Boolean(formatSettings && formatSettings.insertSpaceBeforeFunctionParenthesis),
        insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis),
        insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets),
        insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces),
        insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: Boolean(!formatSettings || formatSettings.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces),
        insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces),
        insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces),
        insertSpaceAfterTypeAssertion: Boolean(formatSettings && formatSettings.insertSpaceAfterTypeAssertion),
        placeOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions),
        placeOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks),
        semicolons: formatSettings === null || formatSettings === void 0 ? void 0 : formatSettings.semicolons
    };
}
function computeInitialIndent(document, range, options) {
    const lineStart = document.offsetAt(languageModes_1.Position.create(range.start.line, 0));
    const content = document.getText();
    let i = lineStart;
    let nChars = 0;
    const tabSize = options.tabSize || 4;
    while (i < content.length) {
        const ch = content.charAt(i);
        if (ch === ' ') {
            nChars++;
        }
        else if (ch === '\t') {
            nChars += tabSize;
        }
        else {
            break;
        }
        i++;
    }
    return Math.floor(nChars / tabSize);
}
function generateIndent(level, options) {
    if (options.insertSpaces) {
        return (0, strings_1.repeat)(' ', level * options.tabSize);
    }
    else {
        return (0, strings_1.repeat)('\t', level);
    }
}
