import { CancellationToken, codefix, Debug, fileShouldUseJavaScriptRequire, findAncestor, findIndex, forEachChild, formatting, getNewLineOrDefaultFromHost, getQuotePreference, getTokenAtPosition, isIdentifier, Program, rangeContainsPosition, rangeContainsRange, SourceFile, Statement, SymbolFlags, textChanges, TextRange, UserPreferences, } from "./_namespaces/ts.js"; import { addTargetFileImports } from "./refactors/helpers.js"; import { addExportsInOldFile, getExistingLocals, getUsageInfo, } from "./refactors/moveToFile.js"; import { CodeFixContextBase, FileTextChanges, LanguageServiceHost, PasteEdits, } from "./types.js"; const fixId = "providePostPasteEdits"; /** @internal */ export function pasteEditsProvider( targetFile: SourceFile, pastedText: string[], pasteLocations: TextRange[], copiedFrom: { file: SourceFile; range: TextRange[]; } | undefined, host: LanguageServiceHost, preferences: UserPreferences, formatContext: formatting.FormatContext, cancellationToken: CancellationToken, ): PasteEdits { const changes: FileTextChanges[] = textChanges.ChangeTracker.with({ host, formatContext, preferences }, changeTracker => pasteEdits(targetFile, pastedText, pasteLocations, copiedFrom, host, preferences, formatContext, cancellationToken, changeTracker)); return { edits: changes, fixId }; } function pasteEdits( targetFile: SourceFile, pastedText: string[], pasteLocations: TextRange[], copiedFrom: { file: SourceFile; range: TextRange[]; } | undefined, host: LanguageServiceHost, preferences: UserPreferences, formatContext: formatting.FormatContext, cancellationToken: CancellationToken, changes: textChanges.ChangeTracker, ) { let actualPastedText: string | undefined; if (pastedText.length !== pasteLocations.length) { actualPastedText = pastedText.length === 1 ? pastedText[0] : pastedText.join(getNewLineOrDefaultFromHost(formatContext.host, formatContext.options)); } const statements: Statement[] = []; let newText = targetFile.text; for (let i = pasteLocations.length - 1; i >= 0; i--) { const { pos, end } = pasteLocations[i]; newText = actualPastedText ? newText.slice(0, pos) + actualPastedText + newText.slice(end) : newText.slice(0, pos) + pastedText[i] + newText.slice(end); } let importAdder: codefix.ImportAdder; Debug.checkDefined(host.runWithTemporaryFileUpdate).call(host, targetFile.fileName, newText, (updatedProgram: Program, originalProgram: Program | undefined, updatedFile: SourceFile) => { importAdder = codefix.createImportAdder(updatedFile, updatedProgram, preferences, host); if (copiedFrom?.range) { Debug.assert(copiedFrom.range.length === pastedText.length); copiedFrom.range.forEach(copy => { const statementsInSourceFile = copiedFrom.file.statements; const startNodeIndex = findIndex(statementsInSourceFile, s => s.end > copy.pos); if (startNodeIndex === -1) return undefined; let endNodeIndex = findIndex(statementsInSourceFile, s => s.end >= copy.end, startNodeIndex); /** * [|console.log(a); * |] * console.log(b); */ if (endNodeIndex !== -1 && copy.end <= statementsInSourceFile[endNodeIndex].getStart()) { endNodeIndex--; } statements.push(...statementsInSourceFile.slice(startNodeIndex, endNodeIndex === -1 ? statementsInSourceFile.length : endNodeIndex + 1)); }); const usage = getUsageInfo(copiedFrom.file, statements, originalProgram!.getTypeChecker(), getExistingLocals(updatedFile, statements, originalProgram!.getTypeChecker()), { pos: copiedFrom.range[0].pos, end: copiedFrom.range[copiedFrom.range.length - 1].end }); Debug.assertIsDefined(originalProgram); const useEsModuleSyntax = !fileShouldUseJavaScriptRequire(targetFile.fileName, originalProgram, host, !!copiedFrom.file.commonJsModuleIndicator); addExportsInOldFile(copiedFrom.file, usage.targetFileImportsFromOldFile, changes, useEsModuleSyntax); addTargetFileImports(copiedFrom.file, usage.oldImportsNeededByTargetFile, usage.targetFileImportsFromOldFile, originalProgram.getTypeChecker(), updatedProgram, importAdder); } else { const context: CodeFixContextBase = { sourceFile: updatedFile, program: originalProgram!, cancellationToken, host, preferences, formatContext, }; // `updatedRanges` represent the new ranges that account for the offset changes caused by pasting new text and // `offset` represents by how much the starting position of `pasteLocations` needs to be changed. // // We iterate over each updated range to get the node that wholly encloses the updated range. // For each child of that node, we checked for unresolved identifiers // within the updated range and try importing it. let offset = 0; pasteLocations.forEach((location, i) => { const oldTextLength = location.end - location.pos; const textToBePasted = actualPastedText ?? pastedText[i]; const startPos = location.pos + offset; const endPos = startPos + textToBePasted.length; const range: TextRange = { pos: startPos, end: endPos }; offset += textToBePasted.length - oldTextLength; const enclosingNode = findAncestor( getTokenAtPosition(context.sourceFile, range.pos), ancestorNode => rangeContainsRange(ancestorNode, range), ); if (!enclosingNode) return; forEachChild(enclosingNode, function importUnresolvedIdentifiers(node) { const isImportCandidate = isIdentifier(node) && rangeContainsPosition(range, node.getStart(updatedFile)) && !updatedProgram?.getTypeChecker().resolveName( node.text, node, SymbolFlags.All, /*excludeGlobals*/ false, ); if (isImportCandidate) { return importAdder.addImportForUnresolvedIdentifier( context, node, /*useAutoImportProvider*/ true, ); } node.forEachChild(importUnresolvedIdentifiers); }); }); } importAdder.writeFixes(changes, getQuotePreference(copiedFrom ? copiedFrom.file : targetFile, preferences)); }); /** * If there are no import fixes, getPasteEdits should return without making any changes to the file. */ if (!importAdder!.hasFixes()) { return; } pasteLocations.forEach((paste, i) => { changes.replaceRangeWithText( targetFile, { pos: paste.pos, end: paste.end }, actualPastedText ?? pastedText[i], ); }); }