"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeActionsProviderImpl = exports.SORT_IMPORT_CODE_ACTION_KIND = void 0;
const typescript_1 = __importDefault(require("typescript"));
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../../lib/documents");
const utils_1 = require("../../../utils");
const DocumentSnapshot_1 = require("../DocumentSnapshot");
const utils_2 = require("../utils");
const utils_3 = require("./utils");
const DiagnosticsProvider_1 = require("./DiagnosticsProvider");
const utils_4 = require("../../../utils");
/**
 * TODO change this to protocol constant if it's part of the protocol
 */
exports.SORT_IMPORT_CODE_ACTION_KIND = 'source.sortImports';
const FIX_IMPORT_FIX_NAME = 'import';
const FIX_IMPORT_FIX_ID = 'fixMissingImport';
const FIX_IMPORT_FIX_DESCRIPTION = 'Add all missing imports';
const nonIdentifierRegex = /[\`\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]/;
class CodeActionsProviderImpl {
    constructor(lsAndTsDocResolver, completionProvider, configManager) {
        this.lsAndTsDocResolver = lsAndTsDocResolver;
        this.completionProvider = completionProvider;
        this.configManager = configManager;
    }
    async getCodeActions(document, range, context, cancellationToken) {
        if (context.only?.[0] === vscode_languageserver_1.CodeActionKind.SourceOrganizeImports) {
            return await this.organizeImports(document, cancellationToken);
        }
        if (context.only?.[0] === exports.SORT_IMPORT_CODE_ACTION_KIND) {
            return await this.organizeImports(document, cancellationToken, 
            /**skipDestructiveCodeActions */ true);
        }
        // for source action command (all source.xxx)
        // vscode would show different source code action kinds to choose from
        if (context.only?.[0] === vscode_languageserver_1.CodeActionKind.Source) {
            return [
                ...(await this.organizeImports(document, cancellationToken)),
                ...(await this.organizeImports(document, cancellationToken, 
                /**skipDestructiveCodeActions */ true))
            ];
        }
        if (context.diagnostics.length &&
            (!context.only || context.only.includes(vscode_languageserver_1.CodeActionKind.QuickFix))) {
            return await this.applyQuickfix(document, range, context, cancellationToken);
        }
        if (!context.only || context.only.includes(vscode_languageserver_1.CodeActionKind.Refactor)) {
            return await this.getApplicableRefactors(document, range, cancellationToken);
        }
        return [];
    }
    async resolveCodeAction(document, codeAction, cancellationToken) {
        if (!this.isQuickFixAllResolveInfo(codeAction.data)) {
            return codeAction;
        }
        const { lang, tsDoc, userPreferences } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
        if (cancellationToken?.isCancellationRequested) {
            return codeAction;
        }
        const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind);
        const formatCodeBasis = (0, utils_3.getFormatCodeBasis)(formatCodeSettings);
        const getDiagnostics = (0, utils_1.memoize)(() => lang.getSemanticDiagnostics(tsDoc.filePath).map((dia) => ({
            range: (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, dia)),
            message: '',
            code: dia.code
        })));
        const isImportFix = codeAction.data.fixName === FIX_IMPORT_FIX_NAME;
        const virtualDocInfo = isImportFix
            ? await this.createVirtualDocumentForCombinedImportCodeFix(document, getDiagnostics(), tsDoc, lang)
            : undefined;
        const fix = lang.getCombinedCodeFix({
            type: 'file',
            fileName: (virtualDocInfo?.virtualDoc ?? document).getFilePath()
        }, codeAction.data.fixId, formatCodeSettings, userPreferences);
        if (virtualDocInfo) {
            const getCanonicalFileName = (0, utils_4.createGetCanonicalFileName)(typescript_1.default.sys.useCaseSensitiveFileNames);
            const virtualDocPath = getCanonicalFileName((0, utils_1.normalizePath)(virtualDocInfo.virtualDoc.getFilePath()));
            for (const change of fix.changes) {
                if (getCanonicalFileName((0, utils_1.normalizePath)(change.fileName)) === virtualDocPath) {
                    change.fileName = tsDoc.filePath;
                    this.removeDuplicatedComponentImport(virtualDocInfo.insertedNames, change);
                }
            }
            await this.lsAndTsDocResolver.deleteSnapshot(virtualDocPath);
        }
        const snapshots = new utils_3.SnapshotMap(this.lsAndTsDocResolver);
        const fixActions = [
            {
                fixName: codeAction.data.fixName,
                changes: Array.from(fix.changes),
                description: ''
            }
        ];
        const documentChangesPromises = fixActions.map((fix) => this.convertAndFixCodeFixAction({
            document,
            fix,
            formatCodeBasis,
            formatCodeSettings,
            getDiagnostics,
            snapshots,
            skipAddScriptTag: true
        }));
        const documentChanges = (await Promise.all(documentChangesPromises)).flat();
        if (cancellationToken?.isCancellationRequested) {
            return codeAction;
        }
        if (isImportFix) {
            this.fixCombinedImportQuickFix(documentChanges, document, formatCodeBasis);
        }
        codeAction.edit = {
            documentChanges
        };
        return codeAction;
    }
    /**
     * Do not use this in regular code action
     * This'll cause TypeScript to rebuild and invalidate caches every time. It'll be slow
     */
    async createVirtualDocumentForCombinedImportCodeFix(document, diagnostics, tsDoc, lang) {
        const virtualUri = document.uri + '.__virtual__.svelte';
        const names = new Set();
        const sourceFile = lang.getProgram()?.getSourceFile(tsDoc.filePath);
        if (!sourceFile) {
            return undefined;
        }
        for (const diagnostic of diagnostics) {
            if (diagnostic.range.start.line < 0 ||
                diagnostic.range.end.line < 0 ||
                diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME) {
                continue;
            }
            const identifier = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile);
            const name = identifier?.text;
            if (!name || names.has(name)) {
                continue;
            }
            if (name.startsWith('$')) {
                names.add(name.slice(1));
            }
            else if (!(0, utils_2.isInScript)(diagnostic.range.start, document)) {
                if (this.isComponentStartTag(identifier)) {
                    names.add((0, utils_2.toGeneratedSvelteComponentName)(name));
                }
            }
        }
        if (!names.size) {
            return undefined;
        }
        const inserts = Array.from(names.values())
            .map((name) => name + ';')
            .join('');
        // assumption: imports are always at the top of the script tag
        // so these appends won't change the position of the edits
        const text = document.getText();
        const newText = document.scriptInfo
            ? text.slice(0, document.scriptInfo.end) + inserts + text.slice(document.scriptInfo.end)
            : `${document.getText()}<script>${inserts}</script>`;
        const virtualDoc = new documents_1.Document(virtualUri, newText);
        // let typescript know about the virtual document
        await this.lsAndTsDocResolver.getSnapshot(virtualDoc);
        return {
            virtualDoc: new documents_1.Document(virtualUri, newText),
            insertedNames: names
        };
    }
    /**
     * Remove component default import if there is a named import with the same name
     * Usually happens with reexport or inheritance of component library
     */
    removeDuplicatedComponentImport(insertedNames, change) {
        for (const name of insertedNames) {
            const unSuffixedNames = (0, utils_2.changeSvelteComponentName)(name);
            const matchRegex = unSuffixedNames != name && this.toImportMemberRegex(unSuffixedNames);
            if (!matchRegex ||
                !change.textChanges.some((textChange) => textChange.newText.match(matchRegex))) {
                continue;
            }
            const importRegex = new RegExp(`\\s+import ${name} from ('|")(.*)('|");?\r?\n?`);
            change.textChanges = change.textChanges
                .map((textChange) => ({
                ...textChange,
                newText: textChange.newText.replace(importRegex, (match) => {
                    if (match.split('\n').length > 2) {
                        return '\n';
                    }
                    else {
                        return '';
                    }
                })
            }))
                // in case there are replacements
                .filter((change) => change.span.length || change.newText);
        }
    }
    fixCombinedImportQuickFix(documentChanges, document, formatCodeBasis) {
        if (!documentChanges.length || document.scriptInfo || document.moduleScriptInfo) {
            return;
        }
        const editForThisFile = documentChanges.find((change) => change.textDocument.uri === document.uri);
        if (editForThisFile?.edits.length) {
            const [first] = editForThisFile.edits;
            first.newText =
                (0, utils_3.getNewScriptStartTag)(this.configManager.getConfig()) +
                    formatCodeBasis.baseIndent +
                    first.newText.trimStart();
            const last = editForThisFile.edits[editForThisFile.edits.length - 1];
            last.newText = last.newText + '</script>' + formatCodeBasis.newLine;
        }
    }
    toImportMemberRegex(name) {
        return new RegExp(`${name}($| |,)`);
    }
    isQuickFixAllResolveInfo(data) {
        const asserted = data;
        return asserted?.fixId != undefined && typeof asserted.fixName === 'string';
    }
    async organizeImports(document, cancellationToken, skipDestructiveCodeActions = false) {
        if (!document.scriptInfo && !document.moduleScriptInfo) {
            return [];
        }
        const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
        if (cancellationToken?.isCancellationRequested || tsDoc.parserError) {
            // If there's a parser error, we fall back to only the script contents,
            // so organize imports likely throws out a lot of seemingly unused imports
            // because they are only used in the template. Therefore do nothing in this case.
            return [];
        }
        const changes = lang.organizeImports({
            fileName: tsDoc.filePath,
            type: 'file',
            skipDestructiveCodeActions
        }, {
            ...(await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind)),
            // handle it on our own
            baseIndentSize: undefined
        }, userPreferences);
        const documentChanges = await Promise.all(changes.map(async (change) => {
            // Organize Imports will only affect the current file, so no need to check the file path
            return vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create(document.url, null), change.textChanges
                .map((edit) => {
                const range = this.checkRemoveImportCodeActionRange(edit, tsDoc, (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, edit.span)));
                edit.newText = (0, utils_1.removeLineWithString)(edit.newText, 'SvelteComponentTyped as __SvelteComponentTyped__');
                return this.fixIndentationOfImports(vscode_languageserver_1.TextEdit.replace(range, edit.newText), document);
            })
                .filter((edit) => 
            // The __SvelteComponentTyped__ import is added by us and will have a negative mapped line
            edit.range.start.line !== -1));
        }));
        return [
            vscode_languageserver_1.CodeAction.create(skipDestructiveCodeActions ? 'Sort Imports' : 'Organize Imports', { documentChanges }, skipDestructiveCodeActions
                ? exports.SORT_IMPORT_CODE_ACTION_KIND
                : vscode_languageserver_1.CodeActionKind.SourceOrganizeImports)
        ];
    }
    fixIndentationOfImports(edit, document) {
        // "Organize Imports" will have edits that delete a group of imports by return empty edits
        // and one edit which contains all the organized imports of the group. Fix indentation
        // of that one by prepending all lines with the indentation of the first line.
        const { newText, range } = edit;
        if (!newText || range.start.character === 0) {
            return edit;
        }
        const line = (0, documents_1.getLineAtPosition)(range.start, document.getText());
        const leadingChars = line.substring(0, range.start.character);
        if (leadingChars.trim() !== '') {
            return edit;
        }
        const fixedNewText = (0, utils_1.modifyLines)(edit.newText, (line, idx) => idx === 0 || !line ? line : leadingChars + line);
        if (range.end.character > 0) {
            const endLine = (0, documents_1.getLineAtPosition)(range.end, document.getText());
            const isIndent = !endLine.substring(0, range.end.character).trim();
            if (isIndent) {
                const trimmedEndLine = endLine.trim();
                // imports that would be removed by the next delete edit
                if (trimmedEndLine && !trimmedEndLine.startsWith('import')) {
                    range.end.character = 0;
                }
            }
        }
        return vscode_languageserver_1.TextEdit.replace(range, fixedNewText);
    }
    checkRemoveImportCodeActionRange(edit, snapshot, range) {
        // Handle svelte2tsx wrong import mapping:
        // The character after the last import maps to the start of the script
        // TODO find a way to fix this in svelte2tsx and then remove this
        if ((range.end.line === 0 && range.end.character === 1) ||
            range.end.line < range.start.line) {
            edit.span.length -= 1;
            range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, edit.span));
            if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
                range.end.character += 1;
                return range;
            }
            const line = (0, documents_1.getLineAtPosition)(range.end, snapshot.getOriginalText());
            // remove-import code action will removes the
            // line break generated by svelte2tsx,
            // but when there's no line break in the source
            // move back to next character would remove the next character
            if ([';', '"', "'"].includes(line[range.end.character])) {
                range.end.character += 1;
            }
            if ((0, documents_1.isAtEndOfLine)(line, range.end.character)) {
                range.end.line += 1;
                range.end.character = 0;
            }
        }
        return range;
    }
    async applyQuickfix(document, range, context, cancellationToken) {
        const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
        if (cancellationToken?.isCancellationRequested) {
            return [];
        }
        const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.start));
        const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end));
        const errorCodes = context.diagnostics.map((diag) => Number(diag.code));
        const cannotFindNameDiagnostic = context.diagnostics.filter((diagnostic) => diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME);
        const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind);
        const formatCodeBasis = (0, utils_3.getFormatCodeBasis)(formatCodeSettings);
        let codeFixes = cannotFindNameDiagnostic.length
            ? this.getComponentImportQuickFix(document, lang, tsDoc, userPreferences, cannotFindNameDiagnostic, formatCodeSettings)
            : undefined;
        codeFixes =
            // either-or situation
            codeFixes || [
                ...lang.getCodeFixesAtPosition(tsDoc.filePath, start, end, errorCodes, formatCodeSettings, userPreferences),
                ...this.getSvelteQuickFixes(lang, document, cannotFindNameDiagnostic, tsDoc, formatCodeBasis, userPreferences, formatCodeSettings)
            ];
        const snapshots = new utils_3.SnapshotMap(this.lsAndTsDocResolver);
        snapshots.set(tsDoc.filePath, tsDoc);
        const codeActionsPromises = codeFixes.map(async (fix) => {
            const documentChanges = await this.convertAndFixCodeFixAction({
                fix,
                snapshots,
                document,
                formatCodeSettings,
                formatCodeBasis,
                getDiagnostics: () => context.diagnostics
            });
            const codeAction = vscode_languageserver_1.CodeAction.create(fix.description, {
                documentChanges
            }, vscode_languageserver_1.CodeActionKind.QuickFix);
            return {
                fix,
                codeAction
            };
        });
        const identifier = {
            uri: document.uri
        };
        const codeActions = await Promise.all(codeActionsPromises);
        if (cancellationToken?.isCancellationRequested) {
            return [];
        }
        const codeActionsNotFilteredOut = codeActions.filter(({ codeAction }) => codeAction.edit?.documentChanges?.every((change) => change.edits.length > 0));
        const fixAllActions = this.getFixAllActions(codeActionsNotFilteredOut.map(({ fix }) => fix), identifier, tsDoc.filePath, lang);
        // filter out empty code action
        return codeActionsNotFilteredOut.map(({ codeAction }) => codeAction).concat(fixAllActions);
    }
    async convertAndFixCodeFixAction({ fix, snapshots, document, formatCodeSettings, formatCodeBasis, getDiagnostics, skipAddScriptTag }) {
        const documentChangesPromises = fix.changes.map(async (change) => {
            const snapshot = await snapshots.retrieve(change.fileName);
            return vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create((0, utils_1.pathToUrl)(change.fileName), null), change.textChanges
                .map((edit) => {
                if (fix.fixName === FIX_IMPORT_FIX_NAME &&
                    snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot) {
                    const namePosition = 'position' in fix ? fix.position : undefined;
                    const startPos = namePosition ??
                        this.findDiagnosticForImportFix(document, edit, getDiagnostics())
                            ?.range?.start ??
                        vscode_languageserver_1.Position.create(0, 0);
                    return this.completionProvider.codeActionChangeToTextEdit(document, snapshot, edit, true, startPos, formatCodeBasis.newLine, undefined, skipAddScriptTag);
                }
                if ((0, utils_3.isTextSpanInGeneratedCode)(snapshot.getFullText(), edit.span)) {
                    return undefined;
                }
                let originalRange = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, edit.span));
                if (fix.fixName === 'unusedIdentifier') {
                    originalRange = this.checkRemoveImportCodeActionRange(edit, snapshot, originalRange);
                }
                if (fix.fixName === 'fixMissingFunctionDeclaration') {
                    const position = 'position' in fix ? fix.position : undefined;
                    const checkRange = position
                        ? vscode_languageserver_1.Range.create(position, position)
                        : this.findDiagnosticForQuickFix(document, DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME, getDiagnostics(), (possiblyIdentifier) => {
                            return edit.newText.includes('function ' + possiblyIdentifier + '(');
                        })?.range;
                    originalRange = this.checkEndOfFileCodeInsert(originalRange, checkRange, document);
                    // ts doesn't add base indent to the first line
                    if (formatCodeSettings.baseIndentSize) {
                        const emptyLine = formatCodeBasis.newLine.repeat(2);
                        edit.newText =
                            emptyLine +
                                formatCodeBasis.baseIndent +
                                edit.newText.trimLeft();
                    }
                }
                if (fix.fixName === 'disableJsDiagnostics') {
                    if (edit.newText.includes('ts-nocheck')) {
                        return this.checkTsNoCheckCodeInsert(document, edit);
                    }
                    return this.checkDisableJsDiagnosticsCodeInsert(originalRange, document, edit);
                }
                if (fix.fixName === 'inferFromUsage') {
                    originalRange = this.checkAddJsDocCodeActionRange(snapshot, originalRange, document);
                }
                if (fix.fixName === 'fixConvertConstToLet') {
                    const offset = document.offsetAt(originalRange.start);
                    const constOffset = document.getText().indexOf('const', offset);
                    if (constOffset < 0) {
                        return undefined;
                    }
                    const beforeConst = document.getText().slice(0, constOffset);
                    if (beforeConst[beforeConst.length - 1] === '@' &&
                        beforeConst
                            .slice(0, beforeConst.length - 1)
                            .trimEnd()
                            .endsWith('{')) {
                        return undefined;
                    }
                }
                if (originalRange.start.line < 0 || originalRange.end.line < 0) {
                    return undefined;
                }
                return vscode_languageserver_1.TextEdit.replace(originalRange, edit.newText);
            })
                .filter(utils_1.isNotNullOrUndefined));
        });
        const documentChanges = await Promise.all(documentChangesPromises);
        return documentChanges;
    }
    findDiagnosticForImportFix(document, edit, diagnostics) {
        return this.findDiagnosticForQuickFix(document, DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME, diagnostics, (possibleIdentifier) => !nonIdentifierRegex.test(possibleIdentifier) &&
            this.toImportMemberRegex(possibleIdentifier).test(edit.newText));
    }
    findDiagnosticForQuickFix(document, targetCode, diagnostics, match) {
        const diagnostic = diagnostics.find((diagnostic) => {
            if (diagnostic.code !== targetCode) {
                return false;
            }
            const possibleIdentifier = document.getText(diagnostic.range);
            if (possibleIdentifier) {
                return match(possibleIdentifier);
            }
            return false;
        });
        return diagnostic;
    }
    getFixAllActions(codeFixes, identifier, fileName, lang) {
        const checkedFixIds = new Set();
        const fixAll = [];
        for (const codeFix of codeFixes) {
            if (!codeFix.fixId || !codeFix.fixAllDescription || checkedFixIds.has(codeFix.fixId)) {
                continue;
            }
            // we have custom fix for import
            // check it again if fix-all might be necessary
            if (codeFix.fixName === FIX_IMPORT_FIX_NAME) {
                const allCannotFindNameDiagnostics = lang
                    .getSemanticDiagnostics(fileName)
                    .filter((diagnostic) => diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME);
                if (allCannotFindNameDiagnostics.length < 2) {
                    checkedFixIds.add(codeFix.fixId);
                    continue;
                }
            }
            const codeAction = vscode_languageserver_1.CodeAction.create(codeFix.fixAllDescription, vscode_languageserver_1.CodeActionKind.QuickFix);
            const data = {
                ...identifier,
                fixName: codeFix.fixName,
                fixId: codeFix.fixId
            };
            codeAction.data = data;
            checkedFixIds.add(codeFix.fixId);
            fixAll.push(codeAction);
        }
        return fixAll;
    }
    /**
     * import quick fix requires the symbol name to be the same as where it's defined.
     * But we have suffix on component default export to prevent conflict with
     * a local variable. So we use auto-import completion as a workaround here.
     */
    getComponentImportQuickFix(document, lang, tsDoc, userPreferences, diagnostics, formatCodeSetting) {
        const sourceFile = lang.getProgram()?.getSourceFile(tsDoc.filePath);
        if (!sourceFile) {
            return;
        }
        const nameToPosition = new Map();
        for (const diagnostic of diagnostics) {
            if ((0, utils_2.isInScript)(diagnostic.range.start, document)) {
                continue;
            }
            const possibleIdentifier = document.getText(diagnostic.range);
            if (!possibleIdentifier ||
                !(0, utils_1.possiblyComponent)(possibleIdentifier) ||
                nameToPosition.has(possibleIdentifier)) {
                continue;
            }
            const node = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile);
            if (!node || !this.isComponentStartTag(node)) {
                return;
            }
            const tagNameEnd = node.getEnd();
            const name = node.getText();
            if ((0, utils_1.possiblyComponent)(name)) {
                nameToPosition.set(name, tagNameEnd);
            }
        }
        if (!nameToPosition.size) {
            return;
        }
        const result = [];
        for (const [name, position] of nameToPosition) {
            const errorPreventingUserPreferences = this.completionProvider.fixUserPreferencesForSvelteComponentImport(userPreferences);
            const resolvedCompletion = (c) => lang.getCompletionEntryDetails(tsDoc.filePath, position, c.name, formatCodeSetting, c.source, errorPreventingUserPreferences, c.data);
            const toFix = (c) => c.codeActions?.map((a) => ({
                ...a,
                description: (0, utils_2.changeSvelteComponentName)(a.description),
                fixName: FIX_IMPORT_FIX_NAME,
                fixId: FIX_IMPORT_FIX_ID,
                fixAllDescription: FIX_IMPORT_FIX_DESCRIPTION,
                position: originalPosition
            })) ?? [];
            const completion = lang.getCompletionsAtPosition(tsDoc.filePath, position, userPreferences, formatCodeSetting);
            const entries = completion?.entries
                .filter((c) => c.name === name || c.name === (0, utils_2.toGeneratedSvelteComponentName)(name))
                .map(resolvedCompletion)
                .sort((a, b) => this.numberOfDirectorySeparators(typescript_1.default.displayPartsToString(a?.sourceDisplay ?? [])) -
                this.numberOfDirectorySeparators(typescript_1.default.displayPartsToString(b?.sourceDisplay ?? [])))
                .filter(utils_1.isNotNullOrUndefined);
            if (!entries?.length) {
                continue;
            }
            const originalPosition = tsDoc.getOriginalPosition(tsDoc.positionAt(position));
            const resultForName = entries.flatMap(toFix);
            result.push(...resultForName);
        }
        return result;
    }
    isComponentStartTag(node) {
        return (typescript_1.default.isCallExpression(node.parent) &&
            typescript_1.default.isIdentifier(node.parent.expression) &&
            node.parent.expression.text === '__sveltets_2_ensureComponent' &&
            typescript_1.default.isIdentifier(node));
    }
    numberOfDirectorySeparators(path) {
        return path.split('/').length - 1;
    }
    getSvelteQuickFixes(lang, document, cannotFindNameDiagnostics, tsDoc, formatCodeBasis, userPreferences, formatCodeSettings) {
        const program = lang.getProgram();
        const sourceFile = program?.getSourceFile(tsDoc.filePath);
        if (!program || !sourceFile) {
            return [];
        }
        const typeChecker = program.getTypeChecker();
        const results = [];
        const quote = (0, utils_3.getQuotePreference)(sourceFile, userPreferences);
        const getGlobalCompletion = (0, utils_1.memoize)(() => lang.getCompletionsAtPosition(tsDoc.filePath, 0, userPreferences, formatCodeSettings));
        const [tsMajorStr] = typescript_1.default.version.split('.');
        const tsSupportHandlerQuickFix = parseInt(tsMajorStr) >= 5;
        for (const diagnostic of cannotFindNameDiagnostics) {
            const identifier = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile);
            if (!identifier) {
                continue;
            }
            const isQuickFixTargetTargetStore = identifier?.escapedText.toString().startsWith('$');
            const fixes = [];
            if (isQuickFixTargetTargetStore) {
                fixes.push(...this.getSvelteStoreQuickFixes(identifier, lang, tsDoc, userPreferences, formatCodeSettings, getGlobalCompletion));
            }
            if (!tsSupportHandlerQuickFix) {
                const isQuickFixTargetEventHandler = this.isQuickFixForEventHandler(document, diagnostic);
                if (isQuickFixTargetEventHandler) {
                    fixes.push(...this.getEventHandlerQuickFixes(identifier, tsDoc, typeChecker, quote, formatCodeBasis));
                }
            }
            if (!fixes.length) {
                continue;
            }
            const originalPosition = tsDoc.getOriginalPosition(tsDoc.positionAt(identifier.pos));
            results.push(...fixes.map((fix) => ({
                name: identifier.getText(),
                position: originalPosition,
                ...fix
            })));
        }
        return results;
    }
    findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile) {
        const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.start));
        const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.end));
        const identifier = (0, utils_3.findClosestContainingNode)(sourceFile, { start, length: end - start }, typescript_1.default.isIdentifier);
        return identifier;
    }
    // TODO: Remove this in late 2023
    // when most users have upgraded to TS 5.0+
    getSvelteStoreQuickFixes(identifier, lang, tsDoc, userPreferences, formatCodeSettings, getCompletions) {
        const storeIdentifier = identifier.escapedText.toString().substring(1);
        const completion = getCompletions();
        if (!completion) {
            return [];
        }
        const toFix = (c) => lang
            .getCompletionEntryDetails(tsDoc.filePath, 0, c.name, formatCodeSettings, c.source, userPreferences, c.data)
            ?.codeActions?.map((a) => ({
            ...a,
            changes: a.changes.map((change) => {
                return {
                    ...change,
                    textChanges: change.textChanges.map((textChange) => {
                        // For some reason, TS sometimes adds the `type` modifier. Remove it.
                        return {
                            ...textChange,
                            newText: textChange.newText.replace(' type ', ' ')
                        };
                    })
                };
            }),
            fixName: FIX_IMPORT_FIX_NAME,
            fixId: FIX_IMPORT_FIX_ID,
            fixAllDescription: FIX_IMPORT_FIX_DESCRIPTION
        })) ?? [];
        return (0, utils_1.flatten)(completion.entries.filter((c) => c.name === storeIdentifier).map(toFix));
    }
    /**
     * Workaround for TypeScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null`
     * We can remove this once TypeScript doesn't have this limitation.
     */
    getEventHandlerQuickFixes(identifier, tsDoc, typeChecker, quote, formatCodeBasis) {
        const type = identifier && typeChecker.getContextualType(identifier);
        // if it's not union typescript should be able to do it. no need to enhance
        if (!type || !type.isUnion()) {
            return [];
        }
        const nonNullable = type.getNonNullableType();
        if (!(nonNullable.flags & typescript_1.default.TypeFlags.Object &&
            nonNullable.objectFlags & typescript_1.default.ObjectFlags.Anonymous)) {
            return [];
        }
        const signature = typeChecker.getSignaturesOfType(nonNullable, typescript_1.default.SignatureKind.Call)[0];
        const parameters = signature.parameters.map((p) => {
            const declaration = p.valueDeclaration ?? p.declarations?.[0];
            const typeString = declaration
                ? typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(p, declaration))
                : '';
            return { name: p.name, typeString };
        });
        const returnType = typeChecker.typeToString(signature.getReturnType());
        const useJsDoc = tsDoc.scriptKind === typescript_1.default.ScriptKind.JS || tsDoc.scriptKind === typescript_1.default.ScriptKind.JSX;
        const parametersText = (useJsDoc
            ? parameters.map((p) => p.name)
            : parameters.map((p) => p.name + (p.typeString ? ': ' + p.typeString : ''))).join(', ');
        const jsDoc = useJsDoc
            ? ['/**', ...parameters.map((p) => ` * @param {${p.typeString}} ${p.name}`), ' */']
            : [];
        const newText = [
            ...jsDoc,
            `function ${identifier.text}(${parametersText})${useJsDoc || returnType === 'any' ? '' : ': ' + returnType} {`,
            formatCodeBasis.indent +
                `throw new Error(${quote}Function not implemented.${quote})` +
                formatCodeBasis.semi,
            '}'
        ]
            .map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
            .join('');
        return [
            {
                description: `Add missing function declaration '${identifier.text}'`,
                fixName: 'fixMissingFunctionDeclaration',
                changes: [
                    {
                        fileName: tsDoc.filePath,
                        textChanges: [
                            {
                                newText,
                                span: { start: 0, length: 0 }
                            }
                        ]
                    }
                ]
            }
        ];
    }
    isQuickFixForEventHandler(document, diagnostic) {
        const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
        if (!htmlNode.attributes ||
            !Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))) {
            return false;
        }
        return true;
    }
    async getApplicableRefactors(document, range, cancellationToken) {
        if (!(0, documents_1.isRangeInTag)(range, document.scriptInfo) &&
            !(0, documents_1.isRangeInTag)(range, document.moduleScriptInfo)) {
            return [];
        }
        // Don't allow refactorings when there is likely a store subscription.
        // Reason: Extracting that would lead to svelte2tsx' transformed store representation
        // showing up, which will confuse the user. In the long run, we maybe have to
        // setup a separate ts language service which only knows of the original script.
        const textInRange = document
            .getText()
            .substring(document.offsetAt(range.start), document.offsetAt(range.end));
        if (textInRange.includes('$')) {
            return [];
        }
        const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
        if (cancellationToken?.isCancellationRequested) {
            return [];
        }
        const textRange = {
            pos: tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.start)),
            end: tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end))
        };
        const applicableRefactors = lang.getApplicableRefactors(document.getFilePath() || '', textRange, userPreferences);
        return (this.applicableRefactorsToCodeActions(applicableRefactors, document, range, textRange)
            // Only allow refactorings from which we know they work
            .filter((refactor) => refactor.command?.command.includes('function_scope') ||
            refactor.command?.command.includes('constant_scope') ||
            refactor.command?.command === 'Infer function return type')
            // The language server also proposes extraction into const/function in module scope,
            // which is outside of the render function, which is svelte2tsx-specific and unmapped,
            // so it would both not work and confuse the user ("What is this render? Never declared that").
            // So filter out the module scope proposal and rename the render-title
            .filter((refactor) => !refactor.title.includes('module scope'))
            .map((refactor) => ({
            ...refactor,
            title: refactor.title
                .replace("Extract to inner function in function 'render'", 'Extract to function')
                .replace("Extract to constant in function 'render'", 'Extract to constant')
        })));
    }
    applicableRefactorsToCodeActions(applicableRefactors, document, originalRange, textRange) {
        return (0, utils_1.flatten)(applicableRefactors.map((applicableRefactor) => {
            if (applicableRefactor.inlineable === false) {
                return [
                    vscode_languageserver_1.CodeAction.create(applicableRefactor.description, {
                        title: applicableRefactor.description,
                        command: applicableRefactor.name,
                        arguments: [
                            document.uri,
                            {
                                type: 'refactor',
                                textRange,
                                originalRange,
                                refactorName: 'Extract Symbol'
                            }
                        ]
                    })
                ];
            }
            return applicableRefactor.actions.map((action) => {
                return vscode_languageserver_1.CodeAction.create(action.description, {
                    title: action.description,
                    command: action.name,
                    arguments: [
                        document.uri,
                        {
                            type: 'refactor',
                            textRange,
                            originalRange,
                            refactorName: applicableRefactor.name
                        }
                    ]
                });
            });
        }));
    }
    async executeCommand(document, command, args) {
        if (!(args?.[1]?.type === 'refactor')) {
            return null;
        }
        const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
        const path = document.getFilePath() || '';
        const { refactorName, originalRange, textRange } = args[1];
        const edits = lang.getEditsForRefactor(path, {}, textRange, refactorName, command, userPreferences);
        if (!edits || edits.edits.length === 0) {
            return null;
        }
        const documentChanges = edits?.edits.map((edit) => vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create(document.uri, null), edit.textChanges.map((edit) => {
            const range = (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, edit.span));
            return vscode_languageserver_1.TextEdit.replace(this.checkEndOfFileCodeInsert(range, originalRange, document), edit.newText);
        })));
        return { documentChanges };
    }
    /**
     * Some refactorings place the new code at the end of svelte2tsx' render function,
     *  which is unmapped. In this case, add it to the end of the script tag ourselves.
     */
    checkEndOfFileCodeInsert(resultRange, targetRange, document) {
        if (resultRange.start.line < 0 || resultRange.end.line < 0) {
            if (document.moduleScriptInfo &&
                (!targetRange || (0, documents_1.isRangeInTag)(targetRange, document.moduleScriptInfo))) {
                return vscode_languageserver_1.Range.create(document.moduleScriptInfo.endPos, document.moduleScriptInfo.endPos);
            }
            if (document.scriptInfo) {
                return vscode_languageserver_1.Range.create(document.scriptInfo.endPos, document.scriptInfo.endPos);
            }
        }
        // don't add script tag here because the code action is calculated
        // when the file is treated as js
        // but user might want a ts version of the code action
        return resultRange;
    }
    checkTsNoCheckCodeInsert(document, edit) {
        const scriptInfo = document.moduleScriptInfo ?? document.scriptInfo;
        if (!scriptInfo) {
            return undefined;
        }
        const newText = typescript_1.default.sys.newLine + edit.newText;
        return vscode_languageserver_1.TextEdit.insert(scriptInfo.startPos, newText);
    }
    checkDisableJsDiagnosticsCodeInsert(originalRange, document, edit) {
        const inModuleScript = (0, documents_1.isInTag)(originalRange.start, document.moduleScriptInfo);
        if (!(0, documents_1.isInTag)(originalRange.start, document.scriptInfo) && !inModuleScript) {
            return null;
        }
        const position = inModuleScript
            ? originalRange.start
            : this.fixPropsCodeActionRange(originalRange.start, document) ?? originalRange.start;
        // fix the length of trailing indent
        const linesOfNewText = edit.newText.split('\n');
        if (/^[ \t]*$/.test(linesOfNewText[linesOfNewText.length - 1])) {
            const line = (0, documents_1.getLineAtPosition)(originalRange.start, document.getText());
            const indent = (0, utils_1.getIndent)(line);
            linesOfNewText[linesOfNewText.length - 1] = indent;
        }
        return vscode_languageserver_1.TextEdit.insert(position, linesOfNewText.join('\n'));
    }
    /**
     * svelte2tsx removes export in instance script
     */
    fixPropsCodeActionRange(start, document) {
        const documentText = document.getText();
        const offset = document.offsetAt(start);
        const exportKeywordOffset = documentText.lastIndexOf('export', offset);
        // export                 let a;
        if (exportKeywordOffset < 0 ||
            documentText.slice(exportKeywordOffset + 'export'.length, offset).trim()) {
            return;
        }
        const charBeforeExport = documentText[exportKeywordOffset - 1];
        if ((charBeforeExport !== undefined && !charBeforeExport.trim()) ||
            charBeforeExport === ';') {
            return document.positionAt(exportKeywordOffset);
        }
    }
    checkAddJsDocCodeActionRange(snapshot, originalRange, document) {
        if (snapshot.scriptKind !== typescript_1.default.ScriptKind.JS &&
            snapshot.scriptKind !== typescript_1.default.ScriptKind.JSX &&
            !(0, documents_1.isInTag)(originalRange.start, document.scriptInfo)) {
            return originalRange;
        }
        const position = this.fixPropsCodeActionRange(originalRange.start, document);
        if (position) {
            return {
                start: position,
                end: position
            };
        }
        return originalRange;
    }
    async getLSAndTSDoc(document) {
        return this.lsAndTsDocResolver.getLSAndTSDoc(document);
    }
}
exports.CodeActionsProviderImpl = CodeActionsProviderImpl;
//# sourceMappingURL=CodeActionsProvider.js.map