"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CompletionsProviderImpl = void 0;
const path_1 = require("path");
const typescript_1 = __importDefault(require("typescript"));
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../../lib/documents");
const parseHtml_1 = require("../../../lib/documents/parseHtml");
const utils_1 = require("../../../utils");
const previewer_1 = require("../previewer");
const utils_2 = require("../utils");
const getJsDocTemplateCompletion_1 = require("./getJsDocTemplateCompletion");
const utils_3 = require("./utils");
const svelte_ast_utils_1 = require("../svelte-ast-utils");
const svelte2tsx_1 = require("svelte2tsx");
const ENSURE_COMPONENT_HELPER = '__sveltets_2_ensureComponent';
class CompletionsProviderImpl {
    constructor(lsAndTsDocResolver, configManager) {
        this.lsAndTsDocResolver = lsAndTsDocResolver;
        this.configManager = configManager;
        /**
         * The language service throws an error if the character is not a valid trigger character.
         * Also, the completions are worse.
         * Therefore, only use the characters the typescript compiler treats as valid.
         */
        this.validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'];
        this.commitCharacters = ['.', ',', ';', '('];
    }
    isValidTriggerCharacter(character) {
        return this.validTriggerCharacters.includes(character);
    }
    async getCompletions(document, position, completionContext, cancellationToken) {
        if ((0, documents_1.isInTag)(position, document.styleInfo)) {
            return null;
        }
        const { lang: langForSyntheticOperations, tsDoc, userPreferences } = await this.lsAndTsDocResolver.getLsForSyntheticOperations(document);
        const filePath = tsDoc.filePath;
        if (!filePath) {
            return null;
        }
        const triggerCharacter = completionContext?.triggerCharacter;
        const triggerKind = completionContext?.triggerKind;
        const validTriggerCharacter = this.isValidTriggerCharacter(triggerCharacter)
            ? triggerCharacter
            : undefined;
        const isCustomTriggerCharacter = triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter;
        const isJsDocTriggerCharacter = triggerCharacter === '*';
        const isEventOrSlotLetTriggerCharacter = triggerCharacter === ':';
        // ignore any custom trigger character specified in server capabilities
        //  and is not allow by ts
        if (isCustomTriggerCharacter &&
            !validTriggerCharacter &&
            !isJsDocTriggerCharacter &&
            !isEventOrSlotLetTriggerCharacter) {
            return null;
        }
        if (this.canReuseLastCompletion(this.lastCompletion, triggerKind, triggerCharacter, document, position)) {
            this.lastCompletion.position = position;
            return this.lastCompletion.completionList;
        }
        else {
            this.lastCompletion = undefined;
        }
        if (!tsDoc.isInGenerated(position)) {
            return null;
        }
        const originalOffset = document.offsetAt(position);
        let offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
        if (isJsDocTriggerCharacter) {
            return (0, getJsDocTemplateCompletion_1.getJsDocTemplateCompletion)(tsDoc, langForSyntheticOperations, filePath, offset);
        }
        const svelteNode = tsDoc.svelteNodeAt(originalOffset);
        if (
        // Cursor is somewhere in regular HTML text
        (svelteNode?.type === 'Text' &&
            [
                'Element',
                'InlineComponent',
                'Fragment',
                'SlotTemplate',
                'SnippetBlock',
                'IfBlock',
                'EachBlock',
                'AwaitBlock',
                'Style'
            ].includes(svelteNode.parent?.type)) ||
            // Cursor is at <div>|</div> in which case there's no TextNode inbetween
            document.getText().substring(originalOffset - 1, originalOffset + 2) === '></') {
            return null;
        }
        const { lang, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
        if (cancellationToken?.isCancellationRequested) {
            return null;
        }
        const inScript = (0, utils_2.isInScript)(position, tsDoc);
        const wordInfo = this.getWordAtPosition(document, originalOffset);
        if (!inScript &&
            wordInfo.word[0] === '{' &&
            (wordInfo.word[1] === '#' ||
                wordInfo.word[1] === '@' ||
                wordInfo.word[1] === ':' ||
                wordInfo.word[1] === '/')) {
            // Typing something like {/if}
            return null;
        }
        // Special case: completion at `<Comp.` -> mapped one character too short -> adjust
        if (!inScript &&
            wordInfo.word === '' &&
            document.getText()[originalOffset - 1] === '.' &&
            tsDoc.getFullText()[offset] === '.') {
            offset++;
        }
        const componentInfo = (0, utils_3.getComponentAtPosition)(lang, document, tsDoc, position);
        const attributeContext = componentInfo && (0, parseHtml_1.getAttributeContextAtPosition)(document, position);
        const eventAndSlotLetCompletions = this.getEventAndSlotLetCompletions(componentInfo, attributeContext, wordInfo.defaultTextEditRange);
        if (isEventOrSlotLetTriggerCharacter) {
            return vscode_languageserver_1.CompletionList.create(eventAndSlotLetCompletions, !!tsDoc.parserError);
        }
        const tagCompletions = componentInfo || eventAndSlotLetCompletions.length > 0
            ? []
            : this.getCustomElementCompletions(lang, lsContainer, document, tsDoc, position);
        const formatSettings = await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind);
        if (cancellationToken?.isCancellationRequested) {
            return null;
        }
        // one or two characters after start tag might be mapped to the component name
        if (svelteNode?.type === 'InlineComponent' &&
            'name' in svelteNode &&
            typeof svelteNode.name === 'string') {
            const name = svelteNode.name;
            const nameEnd = svelteNode.start + 1 + name.length;
            const mightBeAtStartTagWhitespace = this.mightBeAtStartTagWhitespace(document, originalOffset);
            const generatedText = tsDoc.getFullText();
            if (mightBeAtStartTagWhitespace && originalOffset > nameEnd) {
                // If it's in whitespace after the component name in the start tag,
                // this should trigger only directive and prop completions
                // Unless it's correctly mapped to props. Handle it by ourselves to avoid expensive global completions
                if (!generatedText.slice(0, offset).endsWith('props: {')) {
                    return this.getCompletionListForDirectiveOrProps(attributeContext, componentInfo, wordInfo.defaultTextEditRange, eventAndSlotLetCompletions, tsDoc);
                }
            }
            else {
                const componentNameOffByOne = generatedText
                    .slice(0, offset + 1)
                    .endsWith(ENSURE_COMPONENT_HELPER + '(' + name);
                if (componentNameOffByOne) {
                    offset++;
                }
            }
        }
        const response = lang.getCompletionsAtPosition(filePath, offset, {
            ...userPreferences,
            triggerCharacter: validTriggerCharacter
        }, formatSettings);
        const commitCharactersOptions = this.getCommitCharactersOptions(response, tsDoc, position);
        let completions = response?.entries || [];
        const customCompletions = eventAndSlotLetCompletions.concat(tagCompletions ?? []);
        if (completions.length === 0 && customCompletions.length === 0) {
            return tsDoc.parserError ? vscode_languageserver_1.CompletionList.create([], true) : null;
        }
        if (completions.length > 500 &&
            svelteNode?.type === 'Element' &&
            completions[0].kind !== typescript_1.default.ScriptElementKind.memberVariableElement) {
            // False global completions inside element start tag
            return null;
        }
        if (completions.length > 500 &&
            svelteNode?.type === 'InlineComponent' &&
            this.mightBeAtStartTagWhitespace(document, originalOffset)) {
            // Very likely false global completions inside component start tag -> narrow
            return this.getCompletionListForDirectiveOrProps(attributeContext, componentInfo, wordInfo.defaultTextEditRange, eventAndSlotLetCompletions, tsDoc);
        }
        // moved here due to perf reasons
        const existingImports = this.getExistingImports(document);
        const fileUrl = (0, utils_1.pathToUrl)(tsDoc.filePath);
        const isCompletionInTag = (0, svelte_ast_utils_1.isInTag)(svelteNode, originalOffset);
        const isHandlerCompletion = svelteNode?.type === 'EventHandler' && svelteNode.parent?.type === 'Element';
        const preferComponents = wordInfo.word[0] === '<' || inScript;
        const completionItems = customCompletions;
        const isValidCompletion = createIsValidCompletion(document, position, !!tsDoc.parserError);
        const addCompletion = (entry, asStore) => {
            if (isValidCompletion(entry)) {
                let completion = this.toCompletionItem(tsDoc, entry, fileUrl, position, isCompletionInTag, commitCharactersOptions, asStore, existingImports, preferComponents);
                if (completion) {
                    completionItems.push(this.fixTextEditRange(wordInfo.range, (0, documents_1.mapCompletionItemToOriginal)(tsDoc, completion), isHandlerCompletion, completion.textEdit, tsDoc));
                }
            }
        };
        // If completion is about a store which is not imported yet, do another
        // completion request at the beginning of the file to get all global
        // import completions and then filter them down to likely matches.
        if (wordInfo.word.charAt(0) === '$') {
            const storeName = wordInfo.word.substring(1);
            const typeChecker = lang.getProgram()?.getTypeChecker();
            const text = '__sveltets_2_store_get(' + storeName;
            if (!tsDoc.getFullText().includes(text) && typeChecker) {
                const pos = (tsDoc.scriptInfo || tsDoc.moduleScriptInfo)?.endPos ?? {
                    line: 0,
                    character: 0
                };
                const virtualOffset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(pos));
                const storeCompletions = lang.getCompletionsAtPosition(filePath, virtualOffset, {
                    ...userPreferences,
                    includeSymbol: true,
                    triggerCharacter: validTriggerCharacter
                }, formatSettings);
                for (const entry of storeCompletions?.entries || []) {
                    if (entry.symbol &&
                        entry.source &&
                        entry.name.startsWith(storeName) &&
                        !entry.name.startsWith('$') &&
                        !(0, utils_2.isGeneratedSvelteComponentName)(entry.name)) {
                        const aliasedSymbol = entry.symbol.flags & typescript_1.default.SymbolFlags.Alias
                            ? typeChecker.getAliasedSymbol(entry.symbol)
                            : entry.symbol;
                        if (aliasedSymbol.flags & typescript_1.default.SymbolFlags.Variable) {
                            addCompletion(entry, true);
                        }
                    }
                }
            }
        }
        for (const entry of completions) {
            addCompletion(entry, false);
        }
        // Add ./$types imports for SvelteKit since TypeScript is bad at it
        if ((0, path_1.basename)(filePath).startsWith('+')) {
            const $typeImports = new Map();
            for (const c of completionItems) {
                if ((0, utils_3.isKitTypePath)(c.data?.source)) {
                    $typeImports.set(c.label, c);
                }
            }
            for (const $typeImport of $typeImports.values()) {
                // resolve path from filePath to svelte-kit/types
                // src/routes/foo/+page.svelte -> .svelte-kit/types/foo/$types.d.ts
                const routesFolder = document.config?.kit?.files?.routes || 'src/routes';
                const relativeFileName = filePath.split(routesFolder)[1]?.slice(1);
                if (relativeFileName) {
                    const relativePath = (0, path_1.dirname)(relativeFileName) === '.' ? '' : `${(0, path_1.dirname)(relativeFileName)}/`;
                    const modifiedSource = $typeImport.data.source.split('.svelte-kit/types')[0] +
                        // note the missing .d.ts at the end - TS wants it that way for some reason
                        `.svelte-kit/types/${routesFolder}/${relativePath}$types`;
                    completionItems.push({
                        ...$typeImport,
                        // Ensure it's sorted above the other imports
                        sortText: !isNaN(Number($typeImport.sortText))
                            ? String(Number($typeImport.sortText) - 1)
                            : $typeImport.sortText,
                        data: {
                            ...$typeImport.data,
                            __is_sveltekit$typeImport: true,
                            source: modifiedSource,
                            data: undefined
                        }
                    });
                }
            }
        }
        const completionList = vscode_languageserver_1.CompletionList.create(completionItems, !!tsDoc.parserError);
        if (commitCharactersOptions.checkCommitCharacters &&
            commitCharactersOptions.defaultCommitCharacters?.length) {
            const clientSupportsItemsDefault = this.configManager
                .getClientCapabilities()
                ?.textDocument?.completion?.completionList?.itemDefaults?.includes('commitCharacters');
            if (clientSupportsItemsDefault) {
                completionList.itemDefaults = {
                    commitCharacters: commitCharactersOptions.defaultCommitCharacters
                };
            }
            else {
                completionList.items.forEach((item) => {
                    item.commitCharacters ??= commitCharactersOptions.defaultCommitCharacters;
                });
            }
        }
        this.lastCompletion = { key: document.getFilePath() || '', position, completionList };
        return completionList;
    }
    getWordAtPosition(document, offset) {
        const wordRange = (0, documents_1.getWordRangeAt)(document.getText(), offset, {
            left: /[^\s.]+$/,
            right: /[^\w$:]/
        });
        const range = vscode_languageserver_1.Range.create(document.positionAt(wordRange.start), document.positionAt(wordRange.end));
        return {
            wordRange,
            word: document.getText().slice(wordRange.start, wordRange.end),
            range,
            defaultTextEditRange: wordRange.start === wordRange.end ? undefined : range
        };
    }
    mightBeAtStartTagWhitespace(document, originalOffset) {
        return /\s[\s>/]/.test(document.getText().substring(originalOffset - 1, originalOffset + 1));
    }
    canReuseLastCompletion(lastCompletion, triggerKind, triggerCharacter, document, position) {
        return (!!lastCompletion &&
            lastCompletion.key === document.getFilePath() &&
            lastCompletion.position.line === position.line &&
            ((Math.abs(lastCompletion.position.character - position.character) < 2 &&
                (triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerForIncompleteCompletions ||
                    // Special case: `.` is a trigger character, but inside import path completions
                    // it shouldn't trigger another completion because we can reuse the old one
                    (triggerCharacter === '.' &&
                        (0, utils_3.isPartOfImportStatement)(document.getText(), position)))) ||
                // `let:` or `on:` -> up to 3 previous characters allowed
                (Math.abs(lastCompletion.position.character - position.character) < 4 &&
                    triggerCharacter === ':' &&
                    !!(0, documents_1.getNodeIfIsInStartTag)(document.html, document.offsetAt(position)))));
    }
    getExistingImports(document) {
        const rawImports = (0, utils_1.getRegExpMatches)(scriptImportRegex, document.getText()).map((match) => (match[1] ?? match[2]).split(','));
        const tidiedImports = rawImports.flat().map((match) => match.trim());
        return new Set(tidiedImports);
    }
    getEventAndSlotLetCompletions(componentInfo, attributeContext, defaultTextEditRange) {
        if (componentInfo === null) {
            return [];
        }
        if (attributeContext?.inValue) {
            return [];
        }
        return [
            ...componentInfo
                .getEvents()
                .map((event) => this.componentInfoToCompletionEntry(event, 'on:', undefined, defaultTextEditRange)),
            ...componentInfo
                .getSlotLets()
                .map((slot) => this.componentInfoToCompletionEntry(slot, 'let:', undefined, defaultTextEditRange))
        ];
    }
    getCustomElementCompletions(lang, lsContainer, document, tsDoc, position) {
        const offset = document.offsetAt(position);
        const tag = (0, documents_1.getNodeIfIsInTagName)(document.html, offset);
        if (!tag || !tag.tag) {
            return;
        }
        let tags = (0, utils_3.getCustomElementsTags)(lang, lsContainer, tsDoc).filter((t) => t.startsWith(tag.tag));
        const tagNameEnd = tag.start + 1 + (tag.tag?.length ?? 0);
        const replacementRange = (0, documents_1.toRange)(document, tag.start + 1, tagNameEnd);
        return tags.map((tag) => ({
            label: tag,
            data: {
                uri: document.uri,
                position,
                name: tag,
                __is_custom_element: true
            },
            kind: vscode_languageserver_1.CompletionItemKind.Property,
            textEdit: vscode_languageserver_1.TextEdit.replace((0, utils_2.cloneRange)(replacementRange), tag),
            commitCharacters: []
        }));
    }
    componentInfoToCompletionEntry(info, prefix, kind, defaultTextEditRange) {
        const name = prefix + info.name;
        return {
            label: name,
            kind,
            sortText: '-1',
            detail: info.name + ': ' + info.type,
            documentation: info.doc && { kind: vscode_languageserver_1.MarkupKind.Markdown, value: info.doc },
            commitCharacters: [],
            textEdit: defaultTextEditRange
                ? vscode_languageserver_1.TextEdit.replace((0, utils_2.cloneRange)(defaultTextEditRange), name)
                : undefined
        };
    }
    getCompletionListForDirectiveOrProps(attributeContext, componentInfo, defaultTextEditRange, eventAndSlotLetCompletions, tsDoc) {
        const props = (!attributeContext?.inValue &&
            componentInfo
                ?.getProps()
                .map((entry) => this.componentInfoToCompletionEntry(entry, '', vscode_languageserver_1.CompletionItemKind.Field, defaultTextEditRange))) ||
            [];
        return vscode_languageserver_1.CompletionList.create([...eventAndSlotLetCompletions, ...props], !!tsDoc.parserError);
    }
    toCompletionItem(snapshot, comp, uri, position, isCompletionInTag, commitCharactersOptions, asStore, existingImports, preferComponents) {
        const completionLabelAndInsert = this.getCompletionLabelAndInsert(snapshot, comp);
        if (!completionLabelAndInsert) {
            return null;
        }
        let { label, insertText, isSvelteComp, isRunesCompletion, replacementSpan } = completionLabelAndInsert;
        // TS may suggest another Svelte component even if there already exists an import
        // with the same name, because under the hood every Svelte component is postfixed
        // with `__SvelteComponent`. In this case, filter out this completion by returning null.
        if (isSvelteComp && existingImports.has(label)) {
            return null;
        }
        // Remove wrong quotes, for example when using --css-props
        if (isCompletionInTag &&
            !insertText &&
            label[0] === '"' &&
            label[label.length - 1] === '"') {
            label = label.slice(1, -1);
        }
        else if (asStore) {
            // only modify label, so that the data property is untouched, which is important so the resolving still works
            label = `$${label}`;
        }
        const textEdit = replacementSpan
            ? vscode_languageserver_1.TextEdit.replace((0, utils_2.convertRange)(snapshot, replacementSpan), insertText ?? label)
            : undefined;
        const labelDetails = comp.labelDetails ??
            (comp.sourceDisplay
                ? {
                    description: typescript_1.default.displayPartsToString(comp.sourceDisplay)
                }
                : undefined);
        return {
            label,
            insertText,
            kind: (0, utils_2.scriptElementKindToCompletionItemKind)(comp.kind),
            commitCharacters: this.getCommitCharacters(comp, commitCharactersOptions, isSvelteComp),
            // Make sure svelte component and runes take precedence
            sortText: preferComponents && (isRunesCompletion || isSvelteComp) ? '-1' : comp.sortText,
            preselect: preferComponents && (isRunesCompletion || isSvelteComp) ? true : comp.isRecommended,
            insertTextFormat: comp.isSnippet ? vscode_languageserver_1.InsertTextFormat.Snippet : undefined,
            labelDetails,
            textEdit,
            // pass essential data for resolving completion
            data: {
                name: comp.name,
                source: comp.source,
                data: comp.data,
                uri,
                position
            }
        };
    }
    getCompletionLabelAndInsert(snapshot, comp) {
        let { name, insertText, kindModifiers } = comp;
        const isScriptElement = comp.kind === typescript_1.default.ScriptElementKind.scriptElement;
        const hasModifier = Boolean(comp.kindModifiers);
        const isRunesCompletion = name === '$props' || name === '$state' || name === '$derived' || name === '$effect';
        const isSvelteComp = !isRunesCompletion && (0, utils_2.isGeneratedSvelteComponentName)(name);
        if (isSvelteComp) {
            name = (0, utils_2.changeSvelteComponentName)(name);
            if (this.isExistingSvelteComponentImport(snapshot, name, comp.source)) {
                return null;
            }
        }
        if (isScriptElement && hasModifier) {
            const label = kindModifiers && !name.endsWith(kindModifiers) ? name + kindModifiers : name;
            return {
                insertText: name,
                label,
                isSvelteComp,
                isRunesCompletion
            };
        }
        if (comp.replacementSpan) {
            return {
                label: name,
                isSvelteComp,
                isRunesCompletion,
                insertText: insertText ? (0, utils_2.changeSvelteComponentName)(insertText) : undefined,
                replacementSpan: comp.replacementSpan
            };
        }
        const isOptional = kindModifiers
            ?.split(/,|\s+/)
            .includes(typescript_1.default.ScriptElementKindModifier.optionalModifier);
        return {
            label: isOptional ? `${name}?` : name,
            insertText: isOptional ? (insertText ?? name) : insertText, // else label with ? is used if no insertText available,
            isSvelteComp,
            isRunesCompletion
        };
    }
    getCommitCharactersOptions(response, tsDoc, position) {
        if ((!(0, utils_2.isInScript)(position, tsDoc) && tsDoc.parserError) || !response) {
            return {
                checkCommitCharacters: false
            };
        }
        const isNewIdentifierLocation = response.isNewIdentifierLocation;
        // TypeScript 5.7+ reused the same array for different completions
        let defaultCommitCharacters = response.defaultCommitCharacters
            ? Array.from(response.defaultCommitCharacters)
            : undefined;
        if (!isNewIdentifierLocation) {
            // This actually always exists although it's optional in the type, at least in ts 5.6,
            // so our commit characters are mostly fallback for older ts versions
            if (defaultCommitCharacters) {
                // this is controlled by a vscode setting that isn't available in the ts server so it isn't added to the language service
                defaultCommitCharacters?.push('(');
            }
            else {
                defaultCommitCharacters = this.commitCharacters;
            }
        }
        return {
            checkCommitCharacters: true,
            defaultCommitCharacters,
            isNewIdentifierLocation
        };
    }
    getCommitCharacters(entry, options, isSvelteComp) {
        // Because Svelte components take precedence, we leave out commit characters to not auto complete
        // in weird places (e.g. when you have foo.filter(a => a)) and get autocomplete for component A,
        // then a commit character of `.` would auto import the component which is not what we want
        if (isSvelteComp) {
            return ['>'];
        }
        // https://github.com/microsoft/vscode/blob/d012408e88ffabd6456c367df4d343654da2eb10/extensions/typescript-language-features/src/languageFeatures/completions.ts#L504
        if (!options.checkCommitCharacters) {
            return undefined;
        }
        const commitCharacters = entry.commitCharacters;
        // Ambient JS word based suggestions
        const skipCommitCharacters = entry.kind === typescript_1.default.ScriptElementKind.warning ||
            entry.kind === typescript_1.default.ScriptElementKind.string;
        if (commitCharacters) {
            if (!options.isNewIdentifierLocation && !skipCommitCharacters) {
                return commitCharacters.concat('(');
            }
            return commitCharacters;
        }
        return skipCommitCharacters ? [] : undefined;
    }
    isExistingSvelteComponentImport(snapshot, name, source) {
        if (!source)
            return false;
        const importStatement = new RegExp(`import ${name} from ["'\`][\\s\\S]+\\.svelte["'\`]`);
        return importStatement.test(snapshot.getFullText());
    }
    fixTextEditRange(wordRange, completionItem, isHandlerCompletion, generatedTextEdit, tsDoc) {
        if (isHandlerCompletion && completionItem.label.startsWith('on:')) {
            completionItem.textEdit = vscode_languageserver_1.TextEdit.replace((0, utils_2.cloneRange)(wordRange), completionItem.label);
            return completionItem;
        }
        const { textEdit } = completionItem;
        if (!textEdit || !vscode_languageserver_1.TextEdit.is(textEdit)) {
            return completionItem;
        }
        if (vscode_languageserver_1.TextEdit.is(generatedTextEdit)) {
            (0, utils_3.checkRangeMappingWithGeneratedSemi)(textEdit.range, generatedTextEdit.range, tsDoc);
        }
        const { newText, range: { start } } = textEdit;
        //If the textEdit is out of the word range of the triggered position
        // vscode would refuse to show the completions
        // split those edits into additionalTextEdit to fix it
        if (start.line !== wordRange.start.line || start.character > wordRange.start.character) {
            return completionItem;
        }
        textEdit.newText = newText.substring(wordRange.start.character - start.character);
        textEdit.range.start = {
            line: start.line,
            character: wordRange.start.character
        };
        completionItem.additionalTextEdits = [
            vscode_languageserver_1.TextEdit.replace({
                start,
                end: {
                    line: start.line,
                    character: wordRange.start.character
                }
            }, newText.substring(0, wordRange.start.character - start.character))
        ];
        return completionItem;
    }
    /**
     * TypeScript throws a debug assertion error if the importModuleSpecifierEnding config is
     * 'js' and there's an unknown file extension - which is the case for `.svelte`. Therefore
     * rewrite the importModuleSpecifierEnding for this case to silence the error.
     */
    fixUserPreferencesForSvelteComponentImport(userPreferences) {
        if (userPreferences.importModuleSpecifierEnding === 'js') {
            return {
                ...userPreferences,
                importModuleSpecifierEnding: 'index'
            };
        }
        return userPreferences;
    }
    async resolveCompletion(document, completionItem, cancellationToken) {
        const { data: comp } = completionItem;
        const { tsDoc, lang, userPreferences, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
        const filePath = tsDoc.filePath;
        const formatCodeOptions = await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind);
        if (!comp || !filePath || cancellationToken?.isCancellationRequested) {
            return completionItem;
        }
        if (comp.__is_custom_element) {
            const doc = (0, utils_3.getCustomElementsDocument)(lang, lsContainer, tsDoc, comp.name);
            if (doc) {
                completionItem.documentation = { value: doc, kind: vscode_languageserver_1.MarkupKind.Markdown };
            }
            return completionItem;
        }
        const is$typeImport = !!comp.__is_sveltekit$typeImport;
        const errorPreventingUserPreferences = comp.source?.endsWith('.svelte')
            ? this.fixUserPreferencesForSvelteComponentImport(userPreferences)
            : userPreferences;
        const detail = lang.getCompletionEntryDetails(filePath, tsDoc.offsetAt(tsDoc.getGeneratedPosition(comp.position)), comp.name, formatCodeOptions, comp.source, errorPreventingUserPreferences, comp.data);
        if (detail) {
            const { detail: itemDetail, documentation: itemDocumentation } = this.getCompletionDocument(tsDoc, detail, is$typeImport);
            // VSCode + tsserver won't have this pop-in effect
            // because tsserver has internal APIs for caching
            // TODO: consider if we should adopt the internal APIs
            if (detail.sourceDisplay && !completionItem.labelDetails) {
                completionItem.labelDetails = {
                    description: typescript_1.default.displayPartsToString(detail.sourceDisplay)
                };
            }
            completionItem.detail = itemDetail;
            completionItem.documentation = itemDocumentation;
        }
        const actions = detail?.codeActions;
        const isImport = !!detail?.source;
        if (actions) {
            const edit = [];
            const formatCodeBasis = (0, utils_3.getFormatCodeBasis)(formatCodeOptions);
            for (const action of actions) {
                for (const change of action.changes) {
                    edit.push(...this.codeActionChangesToTextEdit(document, tsDoc, change, isImport, comp.position, formatCodeBasis.newLine, is$typeImport));
                }
            }
            completionItem.additionalTextEdits = (completionItem.additionalTextEdits ?? []).concat(edit);
        }
        return completionItem;
    }
    getCompletionDocument(tsDoc, compDetail, is$typeImport) {
        const { sourceDisplay, documentation: tsDocumentation, displayParts, tags } = compDetail;
        let parts = compDetail.codeActions?.map((codeAction) => codeAction.description) ?? [];
        if (sourceDisplay && is$typeImport) {
            const importPath = typescript_1.default.displayPartsToString(sourceDisplay);
            // Take into account Node16 moduleResolution
            parts = parts.map((detail) => detail.replace(importPath, `'./$types${importPath.endsWith('.js') ? '.js' : ''}'`));
        }
        let text = (0, utils_2.changeSvelteComponentName)(typescript_1.default.displayPartsToString(displayParts));
        if (tsDoc.isSvelte5Plus && text.includes('(alias)')) {
            // The info contains both the const and type export along with a bunch of gibberish we want to hide
            if (text.includes('__SvelteComponent_')) {
                // import - remove completely
                text = '';
            }
            else if (text.includes('__sveltets_2_IsomorphicComponent')) {
                // already imported - only keep the last part
                text = text.substring(text.lastIndexOf('import'));
            }
        }
        parts.push(text);
        const markdownDoc = (0, previewer_1.getMarkdownDocumentation)(tsDocumentation, tags);
        const documentation = markdownDoc
            ? { value: markdownDoc, kind: vscode_languageserver_1.MarkupKind.Markdown }
            : undefined;
        return {
            documentation,
            detail: parts.filter(Boolean).join('\n\n')
        };
    }
    codeActionChangesToTextEdit(doc, snapshot, changes, isImport, originalTriggerPosition, newLine, is$typeImport) {
        return changes.textChanges.map((change) => this.codeActionChangeToTextEdit(doc, snapshot, change, isImport, originalTriggerPosition, newLine, is$typeImport));
    }
    codeActionChangeToTextEdit(doc, snapshot, change, isImport, originalTriggerPosition, newLine, is$typeImport, isCombinedCodeAction) {
        change.newText = isCombinedCodeAction
            ? (0, utils_1.modifyLines)(change.newText, (line) => this.fixImportNewText(line, (0, utils_2.isInScript)(originalTriggerPosition, doc), is$typeImport))
            : this.fixImportNewText(change.newText, (0, utils_2.isInScript)(originalTriggerPosition, doc), is$typeImport);
        const scriptTagInfo = snapshot.scriptInfo || snapshot.moduleScriptInfo;
        // no script tag defined yet, add it.
        if (!scriptTagInfo) {
            if (isCombinedCodeAction) {
                return vscode_languageserver_1.TextEdit.insert(vscode_languageserver_1.Position.create(0, 0), change.newText);
            }
            const config = this.configManager.getConfig();
            // Remove the empty line after the script tag because getNewScriptStartTag will always add one
            let newText = change.newText;
            if (newText[0] === '\r') {
                newText = newText.substring(1);
            }
            if (newText[0] === '\n') {
                newText = newText.substring(1);
            }
            return vscode_languageserver_1.TextEdit.replace(beginOfDocumentRange, `${(0, utils_3.getNewScriptStartTag)(config, newLine)}${newText}</script>${newLine}`);
        }
        const { span } = change;
        const virtualRange = (0, utils_2.convertRange)(snapshot, span);
        let range;
        const isNewImport = isImport && virtualRange.start.character === 0;
        // Since new import always can't be mapped, we'll have special treatment here
        //  but only hack this when there is multiple line in script
        if (isNewImport && virtualRange.start.line > 1) {
            range = this.mapRangeForNewImport(snapshot, virtualRange);
        }
        else {
            range = (0, documents_1.mapRangeToOriginal)(snapshot, virtualRange);
        }
        // If range is somehow not mapped in parent,
        // the import is mapped wrong or is outside script tag,
        // use script starting point instead.
        // This happens among other things if the completion is the first import of the file.
        if (range.start.line === -1 ||
            (range.start.line === 0 && range.start.character <= 1 && span.length === 0) ||
            !(0, utils_2.isInScript)(range.start, snapshot)) {
            range = (0, utils_2.convertRange)(doc, {
                start: (0, documents_1.isInTag)(originalTriggerPosition, doc.scriptInfo)
                    ? snapshot.scriptInfo?.start || scriptTagInfo.start
                    : (0, documents_1.isInTag)(originalTriggerPosition, doc.moduleScriptInfo)
                        ? snapshot.moduleScriptInfo?.start || scriptTagInfo.start
                        : scriptTagInfo.start,
                length: span.length
            });
        }
        // prevent newText from being placed like this: <script>import {} from ''
        const editOffset = doc.offsetAt(range.start);
        if ((editOffset === snapshot.scriptInfo?.start ||
            editOffset === snapshot.moduleScriptInfo?.start) &&
            !change.newText.startsWith('\r\n') &&
            !change.newText.startsWith('\n')) {
            change.newText = newLine + change.newText;
        }
        const after = doc.getText().slice(doc.offsetAt(range.end));
        // typescript add empty line after import when the generated ts file
        // doesn't have new line at the start of the file
        if (after.startsWith('\r\n') || after.startsWith('\n')) {
            change.newText = change.newText.trimEnd() + newLine;
        }
        return vscode_languageserver_1.TextEdit.replace(range, change.newText);
    }
    mapRangeForNewImport(snapshot, virtualRange) {
        const sourceMappableRange = this.offsetLinesAndMovetoStartOfLine(virtualRange, -1);
        const mappableRange = (0, documents_1.mapRangeToOriginal)(snapshot, sourceMappableRange);
        return this.offsetLinesAndMovetoStartOfLine(mappableRange, 1);
    }
    offsetLinesAndMovetoStartOfLine({ start, end }, offsetLines) {
        return vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(start.line + offsetLines, 0), vscode_languageserver_1.Position.create(end.line + offsetLines, 0));
    }
    fixImportNewText(importText, actionTriggeredInScript, is$typeImport) {
        if (is$typeImport && importText.trim().startsWith('import ')) {
            // Take into account Node16 moduleResolution
            return importText.replace(/(['"])(.+?)['"]/, (_match, quote, path) => `${quote}./$types${path.endsWith('.js') ? '.js' : ''}${quote}`);
        }
        const changedName = (0, utils_2.changeSvelteComponentName)(importText);
        if (importText !== changedName || !actionTriggeredInScript) {
            // For some reason, TS sometimes adds the `type` modifier. Remove it
            // in case of Svelte component imports or if import triggered from markup.
            return changedName.replace(' type ', ' ');
        }
        return importText;
    }
}
exports.CompletionsProviderImpl = CompletionsProviderImpl;
const beginOfDocumentRange = vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0));
// `import {...} from '..'` or `import ... from '..'`
// Note: Does not take into account if import is within a comment.
const scriptImportRegex = /\bimport\s+{([^}]*?)}\s+?from\s+['"`].+?['"`]|\bimport\s+(\w+?)\s+from\s+['"`].+?['"`]/g;
// Type definitions from svelte-shims.d.ts that shouldn't appear in completion suggestions
// because they are meant to be used "behind the scenes"
const svelte2tsxTypes = new Set([
    'Svelte2TsxComponent',
    'Svelte2TsxComponentConstructorParameters',
    'SvelteComponentConstructor',
    'SvelteActionReturnType',
    'SvelteTransitionConfig',
    'SvelteTransitionReturnType',
    'SvelteAnimationReturnType',
    'SvelteWithOptionalProps',
    'SvelteAllProps',
    'SveltePropsAnyFallback',
    'SvelteSlotsAnyFallback',
    'SvelteRestProps',
    'SvelteSlots',
    'SvelteStore'
]);
const startsWithUppercase = /^[A-Z]/;
function createIsValidCompletion(document, position, hasParserError) {
    // Make fallback completions for tags inside the template a bit better
    const isAtStartTag = !(0, documents_1.isInTag)(position, document.scriptInfo) &&
        /<\w*$/.test(document.getText(vscode_languageserver_1.Range.create(position.line, 0, position.line, position.character)));
    const noWrongCompletionAtStartTag = isAtStartTag && hasParserError
        ? (value) => startsWithUppercase.test(value.name)
        : () => true;
    const isNoSvelte2tsxCompletion = (value) => {
        if (value.kindModifiers === 'declare') {
            return !value.name.startsWith('__sveltets_') && !svelte2tsxTypes.has(value.name);
        }
        return value.name !== svelte2tsx_1.internalHelpers.renderName && !value.name.startsWith('$$_');
    };
    const isCompletionInHTMLStartTag = !!(0, documents_1.getNodeIfIsInHTMLStartTag)(document.html, document.offsetAt(position));
    if (!isCompletionInHTMLStartTag) {
        return isNoSvelte2tsxCompletion;
    }
    // TODO with the new transformation this is ts.ScriptElementKind.memberVariableElement
    // which is also true for all properties of any other object -> how reliably filter this out?
    // ---> another /*ignore*/ pragma?
    // ---> OR: make these lower priority if we find out they are inside a html start tag
    return (value) => isNoSvelte2tsxCompletion(value) && noWrongCompletionAtStartTag(value);
}
//# sourceMappingURL=CompletionProvider.js.map