"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeActionsProviderImpl = exports.REMOVE_UNUSED_IMPORTS_CODE_ACTION_KIND = exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND = exports.SORT_IMPORT_CODE_ACTION_KIND = void 0;
const svelte2tsx_1 = require("svelte2tsx");
const typescript_1 = __importStar(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 DiagnosticsProvider_1 = require("./DiagnosticsProvider");
const utils_3 = require("./utils");
/**
 * TODO change this to protocol constant if it's part of the protocol
 */
exports.SORT_IMPORT_CODE_ACTION_KIND = 'source.sortImports';
exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND = 'source.addMissingImports';
exports.REMOVE_UNUSED_IMPORTS_CODE_ACTION_KIND = 'source.removeUnusedImports';
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, typescript_1.OrganizeImportsMode.SortAndCombine);
        }
        if (context.only?.[0] === exports.REMOVE_UNUSED_IMPORTS_CODE_ACTION_KIND) {
            return await this.organizeImports(document, cancellationToken, typescript_1.OrganizeImportsMode.RemoveUnused);
        }
        if (context.only?.[0] === exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND) {
            return await this.addMissingImports(document, cancellationToken);
        }
        // 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, typescript_1.OrganizeImportsMode.SortAndCombine)),
                ...(await this.organizeImports(document, cancellationToken, typescript_1.OrganizeImportsMode.RemoveUnused)),
                ...(await this.addMissingImports(document, cancellationToken))
            ];
        }
        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, lsContainer } = 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
            ? this.createVirtualDocumentForCombinedImportCodeFix(document, getDiagnostics(), tsDoc, lsContainer, lang)
            : undefined;
        const fix = lang.getCombinedCodeFix({
            type: 'file',
            fileName: (virtualDocInfo?.virtualDoc ?? document).getFilePath()
        }, codeAction.data.fixId, formatCodeSettings, userPreferences);
        if (virtualDocInfo) {
            const getCanonicalFileName = (0, utils_1.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, lsContainer);
        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
     */
    createVirtualDocumentForCombinedImportCodeFix(document, diagnostics, tsDoc, lsContainer, 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 &&
                    diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y)) {
                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, /* skipConfigLoading */ true);
        virtualDoc.openedByClient = true;
        // let typescript know about the virtual document
        lsContainer.openVirtualDocument(virtualDoc);
        lsContainer.getService();
        return {
            virtualDoc,
            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.newLine) +
                    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, mode = typescript_1.OrganizeImportsMode.All) {
        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',
            mode
        }, {
            ...(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));
        }));
        for (const change of documentChanges) {
            this.checkIndentLeftover(change, document);
        }
        let kind;
        let title;
        switch (mode) {
            case typescript_1.OrganizeImportsMode.SortAndCombine:
                kind = exports.SORT_IMPORT_CODE_ACTION_KIND;
                title = 'Sort Imports';
                break;
            case typescript_1.OrganizeImportsMode.RemoveUnused:
                kind = exports.REMOVE_UNUSED_IMPORTS_CODE_ACTION_KIND;
                title = 'Remove Unused Imports';
                break;
            default:
                kind = vscode_languageserver_1.CodeActionKind.SourceOrganizeImports;
                title = 'Organize Imports';
        }
        return [vscode_languageserver_1.CodeAction.create(title, { documentChanges }, kind)];
    }
    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) {
        if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
            return 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 ||
            ((0, utils_2.isInScript)(range.start, snapshot) && !(0, utils_2.isInScript)(range.end, snapshot))) {
            edit.span.length -= 1;
            range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, edit.span));
            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;
    }
    checkIndentLeftover(change, document) {
        if (!change.edits.length) {
            return;
        }
        const orderedByStart = change.edits.sort((a, b) => {
            if (a.range.start.line !== b.range.start.line) {
                return a.range.start.line - b.range.start.line;
            }
            return a.range.start.character - b.range.start.character;
        });
        let current;
        let groups = [];
        for (let i = 0; i < orderedByStart.length; i++) {
            const edit = orderedByStart[i];
            if (!current) {
                current = { range: (0, utils_2.cloneRange)(edit.range), newText: edit.newText };
                continue;
            }
            if ((0, utils_1.isPositionEqual)(current.range.end, edit.range.start)) {
                current.range.end = edit.range.end;
                current.newText += edit.newText;
            }
            else {
                groups.push(current);
                current = { range: (0, utils_2.cloneRange)(edit.range), newText: edit.newText };
            }
        }
        if (current) {
            groups.push(current);
        }
        for (const edit of groups) {
            if (edit.newText) {
                continue;
            }
            const range = edit.range;
            const lineContentBeforeRemove = document.getText({
                start: { line: range.start.line, character: 0 },
                end: range.start
            });
            const onlyIndentLeft = !lineContentBeforeRemove.trim();
            if (!onlyIndentLeft) {
                continue;
            }
            const lineContentAfterRemove = document.getText({
                start: range.end,
                end: { line: range.end.line, character: Number.MAX_VALUE }
            });
            const emptyAfterRemove = !lineContentAfterRemove.trim() || lineContentAfterRemove.startsWith('</script>');
            if (emptyAfterRemove) {
                change.edits.push({
                    range: { start: { line: range.start.line, character: 0 }, end: range.start },
                    newText: ''
                });
            }
        }
    }
    async applyQuickfix(document, range, context, cancellationToken) {
        const { lang, tsDoc, userPreferences, lsContainer } = 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 ||
            diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y);
        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;
        // either-or situation when it's not a "did you mean" fix
        if (codeFixes === undefined ||
            errorCodes.includes(DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y)) {
            codeFixes ??= [];
            codeFixes = codeFixes.concat(...lang.getCodeFixesAtPosition(tsDoc.filePath, start, end, errorCodes, formatCodeSettings, userPreferences), ...this.getSvelteQuickFixes(lang, cannotFindNameDiagnostic, tsDoc, userPreferences, formatCodeSettings));
        }
        const snapshots = new utils_3.SnapshotMap(this.lsAndTsDocResolver, lsContainer);
        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);
        const addLangCodeAction = this.getAddLangTSCodeAction(document, context, tsDoc, formatCodeBasis);
        // filter out empty code action
        const result = codeActionsNotFilteredOut
            .map(({ codeAction }) => codeAction)
            .concat(fixAllActions);
        return addLangCodeAction ? [addLangCodeAction].concat(result) : result;
    }
    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 === 'fixAwaitInSyncFunction' && document.scriptInfo) {
                    const scriptStartTagStart = document.scriptInfo.container.start;
                    const scriptStartTagEnd = document.scriptInfo.start;
                    const withinStartTag = document.offsetAt(originalRange.start) < scriptStartTagEnd &&
                        document.offsetAt(originalRange.end) > scriptStartTagStart;
                    if (withinStartTag) {
                        return undefined;
                    }
                }
                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 ||
                    diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y);
                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, cannotFindNameDiagnostics, tsDoc, userPreferences, formatCodeSettings) {
        const program = lang.getProgram();
        const sourceFile = program?.getSourceFile(tsDoc.filePath);
        if (!program || !sourceFile) {
            return [];
        }
        const results = [];
        const getGlobalCompletion = (0, utils_1.memoize)(() => lang.getCompletionsAtPosition(tsDoc.filePath, 0, userPreferences, formatCodeSettings));
        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 (!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;
    }
    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 completion.entries.filter((c) => c.name === storeIdentifier).flatMap(toFix);
    }
    getAddLangTSCodeAction(document, context, tsDoc, formatCodeBasis) {
        if (tsDoc.scriptKind !== typescript_1.default.ScriptKind.JS) {
            return;
        }
        let hasTSOnlyDiagnostic = false;
        for (const diagnostic of context.diagnostics) {
            const num = Number(diagnostic.code);
            const canOnlyBeUsedInTS = num >= 8004 && num <= 8017;
            if (canOnlyBeUsedInTS) {
                hasTSOnlyDiagnostic = true;
                break;
            }
        }
        if (!hasTSOnlyDiagnostic) {
            return;
        }
        if (!document.scriptInfo && !document.moduleScriptInfo) {
            const hasNonTopLevelLang = document.html.roots.some((node) => this.hasLangTsScriptTag(node));
            // Might be because issue with parsing the script tag, so don't suggest adding a new one
            if (hasNonTopLevelLang) {
                return;
            }
            return vscode_languageserver_1.CodeAction.create('Add <script lang="ts"> tag', {
                documentChanges: [
                    {
                        textDocument: vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create(document.uri, null),
                        edits: [
                            {
                                range: vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0)),
                                newText: '<script lang="ts"></script>' + formatCodeBasis.newLine
                            }
                        ]
                    }
                ]
            }, vscode_languageserver_1.CodeActionKind.QuickFix);
        }
        const edits = [document.scriptInfo, document.moduleScriptInfo]
            .map((info) => {
            if (!info) {
                return;
            }
            const startTagNameEnd = document.positionAt(info.container.start + 7); // <script
            const existingLangOffset = document
                .getText({
                start: startTagNameEnd,
                end: document.positionAt(info.start)
            })
                .indexOf('lang=');
            if (existingLangOffset !== -1) {
                return;
            }
            return {
                range: vscode_languageserver_1.Range.create(startTagNameEnd, startTagNameEnd),
                newText: ' lang="ts"'
            };
        })
            .filter(utils_1.isNotNullOrUndefined);
        if (edits.length) {
            return vscode_languageserver_1.CodeAction.create('Add lang="ts" to <script> tag', {
                documentChanges: [
                    {
                        textDocument: vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create(document.uri, null),
                        edits
                    }
                ]
            }, vscode_languageserver_1.CodeActionKind.QuickFix);
        }
    }
    hasLangTsScriptTag(node) {
        if (node.tag === 'script' &&
            (node.attributes?.lang === '"ts"' || node.attributes?.lang === "'ts'") &&
            node.parent) {
            return true;
        }
        for (const element of node.children) {
            if (this.hasLangTsScriptTag(element)) {
                return true;
            }
        }
        return false;
    }
    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 '${svelte2tsx_1.internalHelpers.renderName}'`, 'Extract to function')
                .replace(`Extract to constant in function '${svelte2tsx_1.internalHelpers.renderName}'`, 'Extract to constant')
        })));
    }
    applicableRefactorsToCodeActions(applicableRefactors, document, originalRange, textRange) {
        return applicableRefactors.flatMap((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);
    }
    async addMissingImports(document, cancellationToken) {
        // Re-introduce LS/TSDoc resolution and diagnostic check
        const { lang, tsDoc } = await this.getLSAndTSDoc(document);
        if (cancellationToken?.isCancellationRequested) {
            return [];
        }
        // Check if there are any relevant "cannot find name" diagnostics
        const diagnostics = lang.getSemanticDiagnostics(tsDoc.filePath);
        const hasMissingImports = diagnostics.some((diag) => (diag.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME ||
            diag.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y) &&
            // Ensure the diagnostic is not in generated code
            !(0, utils_3.isTextSpanInGeneratedCode)(tsDoc.getFullText(), {
                start: diag.start ?? 0,
                length: diag.length ?? 0
            }));
        // Only return the action if there are potential imports to add
        if (!hasMissingImports) {
            return [];
        }
        // If imports might be needed, create the deferred action
        const codeAction = vscode_languageserver_1.CodeAction.create(FIX_IMPORT_FIX_DESCRIPTION, exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND);
        const data = {
            uri: document.uri,
            fixName: FIX_IMPORT_FIX_NAME,
            fixId: FIX_IMPORT_FIX_ID
        };
        codeAction.data = data;
        return [codeAction];
    }
}
exports.CodeActionsProviderImpl = CodeActionsProviderImpl;
//# sourceMappingURL=CodeActionsProvider.js.map