diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index d5460a49e3928..27787a6fa9e60 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -7600,6 +7600,10 @@ "category": "Message", "code": 95177 }, + "Move to file": { + "category": "Message", + "code": 95178 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 7da8fba65d3a2..c16abc9a809c7 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -151,11 +151,18 @@ function getPreferences( ? [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Index] : [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension]; } + const allowImportingTsExtension = shouldAllowImportingTsExtension(compilerOptions, importingSourceFile.fileName); switch (preferredEnding) { - case ModuleSpecifierEnding.JsExtension: return [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index]; + case ModuleSpecifierEnding.JsExtension: return allowImportingTsExtension + ? [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index] + : [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index]; case ModuleSpecifierEnding.TsExtension: return [ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Index]; - case ModuleSpecifierEnding.Index: return [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension]; - case ModuleSpecifierEnding.Minimal: return [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension]; + case ModuleSpecifierEnding.Index: return allowImportingTsExtension + ? [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.JsExtension] + : [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension]; + case ModuleSpecifierEnding.Minimal: return allowImportingTsExtension + ? [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.JsExtension] + : [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension]; default: Debug.assertNever(preferredEnding); } }, @@ -1046,7 +1053,12 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie return fileName; } - if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) { + const jsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension); + const tsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.TsExtension); + if (fileExtensionIsOneOf(fileName, [Extension.Mts, Extension.Cts]) && tsPriority !== -1 && tsPriority < jsPriority) { + return fileName; + } + else if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) { return noExtension + getJSExtensionForFile(fileName, options); } else if (!fileExtensionIsOneOf(fileName, [Extension.Dts]) && fileExtensionIsOneOf(fileName, [Extension.Ts]) && stringContains(fileName, ".d.")) { @@ -1072,7 +1084,6 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie // know if a .d.ts extension is valid, so use no extension or a .js extension if (isDeclarationFileName(fileName)) { const extensionlessPriority = allowedEndings.findIndex(e => e === ModuleSpecifierEnding.Minimal || e === ModuleSpecifierEnding.Index); - const jsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension); return extensionlessPriority !== -1 && extensionlessPriority < jsPriority ? noExtension : noExtension + getJSExtensionForFile(fileName, options); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f297ed469466e..0b09674a3b317 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4239,7 +4239,6 @@ export interface SourceFileLike { getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number; } - /** @internal */ export interface RedirectInfo { /** Source file this redirects to. */ diff --git a/src/harness/client.ts b/src/harness/client.ts index a74e616072302..b68f4791f4bdb 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -793,6 +793,14 @@ export class SessionClient implements LanguageService { return response.body!; // TODO: GH#18217 } + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange): { newFileName: string; files: string[]; } { + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); + + const request = this.processRequest(protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, args); + const response = this.processResponse(request); + return { newFileName: response.body?.newFileName, files:response.body?.files }!; + } + getEditsForRefactor( fileName: string, _formatOptions: FormatCodeSettings, @@ -816,7 +824,7 @@ export class SessionClient implements LanguageService { const renameFilename: string | undefined = response.body.renameFilename; let renameLocation: number | undefined; if (renameFilename !== undefined) { - renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 + renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); } return { diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 85f8bd1118cc1..03d3b52afe7b3 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3293,7 +3293,6 @@ export class TestState { ts.Debug.fail(`Did not expect a change in ${change.fileName}`); } const oldText = this.tryGetFileContent(change.fileName); - ts.Debug.assert(!!change.isNewFile === (oldText === undefined)); const newContent = change.isNewFile ? ts.first(change.textChanges).newText : ts.textChanges.applyChanges(oldText!, change.textChanges); this.verifyTextMatches(newContent, /*includeWhitespace*/ true, expectedNewContent); } @@ -3906,6 +3905,18 @@ export class TestState { this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits); } + public moveToFile(options: FourSlashInterface.MoveToFileOptions): void { + assert(this.getRanges().length === 1, "Must have exactly one fourslash range (source enclosed between '[|' and '|]' delimiters) in the source file"); + const range = this.getRanges()[0]; + const refactor = ts.find(this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true }, /*triggerReason*/ undefined, /*kind*/ undefined, /*includeInteractiveActions*/ true), r => r.name === "Move to file")!; + assert(refactor.actions.length === 1); + const action = ts.first(refactor.actions); + assert(action.name === "Move to file" && action.description === "Move to file"); + + const editInfo = this.languageService.getEditsForRefactor(range.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.emptyOptions, options.interactiveRefactorArguments)!; + this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits); + } + private testNewFileContents(edits: readonly ts.FileTextChanges[], newFileContents: { [fileName: string]: string }, description: string): void { for (const { fileName, textChanges } of edits) { const newContent = newFileContents[fileName]; @@ -4211,11 +4222,11 @@ export class TestState { private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, preferences = ts.emptyOptions) { return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, preferences, triggerReason, kind); } - private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string): readonly ts.ApplicableRefactorInfo[] { - return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind); // eslint-disable-line local/no-in-operator + private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, includeInteractiveActions?: boolean): readonly ts.ApplicableRefactorInfo[] { + return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind, includeInteractiveActions); // eslint-disable-line local/no-in-operator } - private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string): readonly ts.ApplicableRefactorInfo[] { - return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind) || ts.emptyArray; + private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string, includeInteractiveActions?: boolean): readonly ts.ApplicableRefactorInfo[] { + return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind, includeInteractiveActions) || ts.emptyArray; } public configurePlugin(pluginName: string, configuration: any): void { diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index d7ad6357b83c9..7bb2d379130a3 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -610,6 +610,10 @@ export class Verify extends VerifyNegatable { this.state.moveToNewFile(options); } + public moveToFile(options: MoveToFileOptions): void { + this.state.moveToFile(options); + } + public noMoveToNewFile(): void { this.state.noMoveToNewFile(); } @@ -1896,6 +1900,12 @@ export interface MoveToNewFileOptions { readonly preferences?: ts.UserPreferences; } +export interface MoveToFileOptions { + readonly newFileContents: { readonly [fileName: string]: string }; + readonly interactiveRefactorArguments: ts.InteractiveRefactorArguments; + readonly preferences?: ts.UserPreferences; +} + export type RenameLocationsOptions = readonly RenameLocationOptions[] | { readonly findInStrings?: boolean; readonly findInComments?: boolean; diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index a8c854325a266..448d3a6f4bc44 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -616,6 +616,9 @@ class LanguageServiceShimProxy implements ts.LanguageService { getApplicableRefactors(): ts.ApplicableRefactorInfo[] { throw new Error("Not supported on the shim."); } + getMoveToRefactoringFileSuggestions(): { newFileName: string, files: string[] } { + throw new Error("Not supported on the shim."); + } organizeImports(_args: ts.OrganizeImportsArgs, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { throw new Error("Not supported on the shim."); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 6f08f3daabf5f..4e4432d0bc2e7 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -4,6 +4,7 @@ import type { EndOfLineState, FileExtensionInfo, HighlightSpanKind, + InteractiveRefactorArguments, MapLike, OutliningSpanKind, OutputFile, @@ -142,6 +143,7 @@ export const enum CommandTypes { GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", + GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions", /** @internal */ GetEditsForRefactorFull = "getEditsForRefactor-full", @@ -606,6 +608,27 @@ export interface GetApplicableRefactorsResponse extends Response { body?: ApplicableRefactorInfo[]; } +/** + * Request refactorings at a given position or selection area to move to an existing file. + */ +export interface GetMoveToRefactoringFileSuggestionsRequest extends Request { + command: CommandTypes.GetMoveToRefactoringFileSuggestions; + arguments: GetMoveToRefactoringFileSuggestionsRequestArgs; +} +export type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRangeRequestArgs & { + kind?: string; +}; +/** + * Response is a list of available files. + * Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring + */ +export interface GetMoveToRefactoringFileSuggestions extends Response { + body: { + newFileName: string; + files: string[]; + }; +} + /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ @@ -680,6 +703,8 @@ export type GetEditsForRefactorRequestArgs = FileLocationOrRangeRequestArgs & { refactor: string; /* The 'name' property from the refactoring action */ action: string; + /* Arguments for interactive action */ + interactiveRefactorArguments?: InteractiveRefactorArguments; }; diff --git a/src/server/session.ts b/src/server/session.ts index 030a1941ccd65..b2369f8b8ebc3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -882,6 +882,7 @@ const invalidPartialSemanticModeCommands: readonly protocol.CommandTypes[] = [ protocol.CommandTypes.ApplyCodeActionCommand, protocol.CommandTypes.GetSupportedCodeFixes, protocol.CommandTypes.GetApplicableRefactors, + protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, protocol.CommandTypes.GetEditsForRefactor, protocol.CommandTypes.GetEditsForRefactorFull, protocol.CommandTypes.OrganizeImports, @@ -2688,6 +2689,7 @@ export class Session implements EventSender { args.refactor, args.action, this.getPreferences(file), + args.interactiveRefactorArguments ); if (result === undefined) { @@ -2703,11 +2705,19 @@ export class Session implements EventSender { const renameScriptInfo = project.getScriptInfoForNormalizedPath(toNormalizedPath(renameFilename))!; mappedRenameLocation = getLocationInNewDocument(getSnapshotText(renameScriptInfo.getSnapshot()), renameFilename, renameLocation, edits); } - return { renameLocation: mappedRenameLocation, renameFilename, edits: this.mapTextChangesToCodeEdits(edits) }; - } - else { - return result; + return { + renameLocation: mappedRenameLocation, + renameFilename, + edits: this.mapTextChangesToCodeEdits(edits) + }; } + return result; + } + + private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFileName: string, files: string[] }{ + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + return project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file)); } private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] { @@ -3430,6 +3440,9 @@ export class Session implements EventSender { [protocol.CommandTypes.GetEditsForRefactor]: (request: protocol.GetEditsForRefactorRequest) => { return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ true)); }, + [protocol.CommandTypes.GetMoveToRefactoringFileSuggestions]: (request: protocol.GetMoveToRefactoringFileSuggestionsRequest) => { + return this.requiredResponse(this.getMoveToRefactoringFileSuggestions(request.arguments)); + }, [protocol.CommandTypes.GetEditsForRefactorFull]: (request: protocol.GetEditsForRefactorRequest) => { return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ false)); }, diff --git a/src/services/_namespaces/ts.refactor.ts b/src/services/_namespaces/ts.refactor.ts index 19159057c6da6..2edf81a5b8caa 100644 --- a/src/services/_namespaces/ts.refactor.ts +++ b/src/services/_namespaces/ts.refactor.ts @@ -6,6 +6,7 @@ export * from "../refactors/convertImport"; export * from "../refactors/extractType"; export * from "../refactors/helpers"; export * from "../refactors/moveToNewFile"; +export * from "../refactors/moveToFile"; import * as addOrRemoveBracesToArrowFunction from "./ts.refactor.addOrRemoveBracesToArrowFunction"; export { addOrRemoveBracesToArrowFunction }; import * as convertArrowFunctionOrFunctionExpression from "./ts.refactor.convertArrowFunctionOrFunctionExpression"; diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index f8ef0d5e7f8bb..a540be35b511b 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -2,6 +2,7 @@ import { ApplicableRefactorInfo, arrayFrom, flatMapIterator, + InteractiveRefactorArguments, Refactor, RefactorContext, RefactorEditInfo, @@ -30,7 +31,7 @@ export function getApplicableRefactors(context: RefactorContext, includeInteract } /** @internal */ -export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined { +export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string, interactiveRefactorArguments?: InteractiveRefactorArguments): RefactorEditInfo | undefined { const refactor = refactors.get(refactorName); - return refactor && refactor.getEditsForAction(context, actionName); + return refactor && refactor.getEditsForAction(context, actionName, interactiveRefactorArguments); } diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts new file mode 100644 index 0000000000000..20ee5c05647d1 --- /dev/null +++ b/src/services/refactors/moveToFile.ts @@ -0,0 +1,1106 @@ +import { getModuleSpecifier } from "../../compiler/moduleSpecifiers"; +import { + AnyImportOrRequireStatement, + append, + ApplicableRefactorInfo, + AssignmentDeclarationKind, + BinaryExpression, + BindingElement, + BindingName, + CallExpression, + canHaveDecorators, + canHaveModifiers, + canHaveSymbol, + cast, + ClassDeclaration, + codefix, + combinePaths, + concatenate, + contains, + createModuleSpecifierResolutionHost, + createTextRangeFromSpan, + Debug, + Declaration, + DeclarationStatement, + Diagnostics, + emptyArray, + EnumDeclaration, + escapeLeadingUnderscores, + Expression, + ExpressionStatement, + extensionFromPath, + ExternalModuleReference, + factory, + fileShouldUseJavaScriptRequire, + find, + FindAllReferences, + findIndex, + firstDefined, + flatMap, + forEachKey, + FunctionDeclaration, + getAssignmentDeclarationKind, + GetCanonicalFileName, + getDecorators, + getDirectoryPath, + getLocaleSpecificMessage, + getModeForUsageLocation, + getModifiers, + getPropertySymbolFromBindingElement, + getQuotePreference, + getRangesWhere, + getRefactorContextSpan, + getRelativePathFromFile, + getSynthesizedDeepClone, + getUniqueName, + hasSyntacticModifier, + hostGetCanonicalFileName, + Identifier, + ImportDeclaration, + ImportEqualsDeclaration, + insertImports, + InterfaceDeclaration, + InternalSymbolName, + isArrayLiteralExpression, + isBinaryExpression, + isBindingElement, + isDeclarationName, + isExpressionStatement, + isExternalModuleReference, + isIdentifier, + isImportDeclaration, + isImportEqualsDeclaration, + isNamedDeclaration, + isObjectLiteralExpression, + isOmittedExpression, + isPrologueDirective, + isPropertyAccessExpression, + isPropertyAssignment, + isRequireCall, + isSourceFile, + isStringLiteral, + isStringLiteralLike, + isValidTypeOnlyAliasUseSite, + isVariableDeclaration, + isVariableDeclarationList, + isVariableStatement, + LanguageServiceHost, + last, + length, + makeImportIfNecessary, + makeStringLiteral, + mapDefined, + ModifierFlags, + ModifierLike, + ModuleDeclaration, + NamedImportBindings, + Node, + NodeFlags, + nodeSeenTracker, + normalizePath, + ObjectBindingElementWithoutPropertyName, + Program, + PropertyAccessExpression, + PropertyAssignment, + QuotePreference, + rangeContainsRange, + RefactorContext, + RefactorEditInfo, + RequireOrImportCall, + RequireVariableStatement, + resolvePath, + ScriptTarget, + skipAlias, + some, + SourceFile, + Statement, + StringLiteralLike, + Symbol, + SymbolFlags, + symbolNameNoDefault, + SyntaxKind, + takeWhile, + textChanges, + TransformFlags, + tryCast, + TypeAliasDeclaration, + TypeChecker, + TypeNode, + UserPreferences, + VariableDeclaration, + VariableDeclarationList, + VariableStatement, +} from "../_namespaces/ts"; +import { registerRefactor } from "../refactorProvider"; + +const refactorNameForMoveToFile = "Move to file"; +const description = getLocaleSpecificMessage(Diagnostics.Move_to_file); + +const moveToFileAction = { + name: "Move to file", + description, + kind: "refactor.move.file", +}; +registerRefactor(refactorNameForMoveToFile, { + kinds: [moveToFileAction.kind], + getAvailableActions: function getRefactorActionsToMoveToFile(context, interactiveRefactorArguments): readonly ApplicableRefactorInfo[] { + const statements = getStatementsToMove(context); + if (!interactiveRefactorArguments) { + return emptyArray; + } + if (context.preferences.allowTextChangesInNewFiles && statements) { + return [{ name: refactorNameForMoveToFile, description, actions: [moveToFileAction] }]; + } + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ name: refactorNameForMoveToFile, description, actions: + [{ ...moveToFileAction, notApplicableReason: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_statement_or_statements) }] + }]; + } + return emptyArray; + }, + getEditsForAction: function getRefactorEditsToMoveToFile(context, actionName, interactiveRefactorArguments): RefactorEditInfo | undefined { + Debug.assert(actionName === refactorNameForMoveToFile, "Wrong refactor invoked"); + const statements = Debug.checkDefined(getStatementsToMove(context)); + Debug.assert(interactiveRefactorArguments, "No interactive refactor arguments available"); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context, context.file, interactiveRefactorArguments.targetFile, context.program, statements, t, context.host, context.preferences)); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } +}); + +function doChange(context: RefactorContext, oldFile: SourceFile, targetFile: string, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { + const checker = program.getTypeChecker(); + const usage = getUsageInfo(oldFile, toMove.all, checker); + //For a new file or an existing blank target file + if (!host.fileExists(targetFile) || host.fileExists(targetFile) && program.getSourceFile(targetFile)?.statements.length === 0) { + changes.createNewFile(oldFile, targetFile, getNewStatementsAndRemoveFromOldFile(oldFile, targetFile, usage, changes, toMove, program, host, preferences)); + addNewFileToTsconfig(program, changes, oldFile.fileName, targetFile, hostGetCanonicalFileName(host)); + } + else { + const targetSourceFile = Debug.checkDefined(program.getSourceFile(targetFile)); + const importAdder = codefix.createImportAdder(targetSourceFile, context.program, context.preferences, context.host); + getNewStatementsAndRemoveFromOldFile(oldFile, targetSourceFile, usage, changes, toMove, program, host, preferences, importAdder); + } +} + +function getNewStatementsAndRemoveFromOldFile( + oldFile: SourceFile, targetFile: string | SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, preferences: UserPreferences, importAdder?: codefix.ImportAdder +) { + const checker = program.getTypeChecker(); + const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); + if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByTargetFile.size === 0 && usage.targetFileImportsFromOldFile.size === 0 && typeof targetFile === "string") { + deleteMovedStatements(oldFile, toMove.ranges, changes); + return [...prologueDirectives, ...toMove.all]; + } + + //If the targetFile is a string, it’s the file name for a new file, if it’s a SourceFile, it’s the existing target file. + const targetFileName = typeof targetFile === "string" ? targetFile : targetFile.fileName; + + const useEsModuleSyntax = !fileShouldUseJavaScriptRequire(targetFileName, program, host, !!oldFile.commonJsModuleIndicator); + const quotePreference = getQuotePreference(oldFile, preferences); + const importsFromTargetFile = createOldFileImportsFromTargetFile(oldFile, usage.oldFileImportsFromTargetFile, targetFileName, program, host, useEsModuleSyntax, quotePreference); + if (importsFromTargetFile) { + insertImports(changes, oldFile, importsFromTargetFile, /*blankLineBetween*/ true, preferences); + } + + deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); + deleteMovedStatements(oldFile, toMove.ranges, changes); + updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, targetFileName, quotePreference); + + const imports = getTargetFileImportsAndAddExportInOldFile(oldFile, targetFileName, usage.oldImportsNeededByTargetFile, usage.targetFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference, importAdder); + const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromTargetFile, useEsModuleSyntax); + if (typeof targetFile !== "string") { + if (targetFile.statements.length > 0) { + changes.insertNodesAfter(targetFile, targetFile.statements[targetFile.statements.length - 1], body); + } + if (imports.length > 0) { + insertImports(changes, targetFile, imports, /*blankLineBetween*/ true, preferences); + } + } + if (importAdder) { + importAdder.writeFixes(changes); + } + if (imports.length && body.length) { + return [ + ...prologueDirectives, + ...imports, + SyntaxKind.NewLineTrivia as const, + ...body + ]; + } + + return [ + ...prologueDirectives, + ...imports, + ...body, + ]; +} + +function getTargetFileImportsAndAddExportInOldFile( + oldFile: SourceFile, + targetFile: string, + importsToCopy: Map, + targetFileImportsFromOldFile: Set, + changes: textChanges.ChangeTracker, + checker: TypeChecker, + program: Program, + host: LanguageServiceHost, + useEsModuleSyntax: boolean, + quotePreference: QuotePreference, + importAdder?: codefix.ImportAdder, +): readonly AnyImportOrRequireStatement[] { + const copiedOldImports: AnyImportOrRequireStatement[] = []; + /** + * Recomputing the imports is preferred with importAdder because it manages multiple import additions for a file and writes then to a ChangeTracker, + * but sometimes it fails because of unresolved imports from files, or when a source file is not available for the target file (in this case when creating a new file). + * So in that case, fall back to copying the import verbatim. + */ + if (importAdder) { + importsToCopy.forEach((isValidTypeOnlyUseSite, symbol) => { + try { + importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker), isValidTypeOnlyUseSite); + } + catch { + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + append(copiedOldImports, filterImport(i, factory.createStringLiteral(moduleSpecifierFromImport(i).text), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + }); + } + } + }); + } + else { + const targetSourceFile = program.getSourceFile(targetFile); // Would be undefined for a new file + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + // Recomputing module specifier + const moduleSpecifier = moduleSpecifierFromImport(i); + const resolved = oldFile.resolvedModules?.get(moduleSpecifier.text, getModeForUsageLocation(oldFile, moduleSpecifier)); + const fileName = resolved?.resolvedModule?.resolvedFileName; + if (fileName && targetSourceFile) { + const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), targetSourceFile, targetSourceFile.path, fileName, createModuleSpecifierResolutionHost(program, host)); + append(copiedOldImports, filterImport(i, makeStringLiteral(newModuleSpecifier, quotePreference), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + } + else { + append(copiedOldImports, filterImport(i, factory.createStringLiteral(moduleSpecifierFromImport(i).text), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + } + }); + } + } + + //Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. + const targetFileSourceFile = program.getSourceFile(targetFile); + let oldFileDefault: Identifier | undefined; + const oldFileNamedImports: string[] = []; + const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. + targetFileImportsFromOldFile.forEach(symbol => { + if (!symbol.declarations) { + return; + } + for (const decl of symbol.declarations) { + if (!isTopLevelDeclaration(decl)) continue; + const name = nameOfTopLevelDeclaration(decl); + if (!name) continue; + + const top = getTopLevelDeclarationStatement(decl); + if (markSeenTop(top)) { + addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax); + } + if (importAdder && checker.isUnknownSymbol(symbol)) { + importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); + } + else { + if (hasSyntacticModifier(decl, ModifierFlags.Default)) { + oldFileDefault = name; + } + else { + oldFileNamedImports.push(name.text); + } + } + } + }); + return (targetFileSourceFile) + ? append(copiedOldImports, makeImportOrRequire(targetFileSourceFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)) + : append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)); +} + +/** @internal */ +export function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { + const cfg = program.getCompilerOptions().configFile; + if (!cfg) return; + + const newFileAbsolutePath = normalizePath(combinePaths(oldFileName, "..", newFileNameWithExtension)); + const newFilePath = getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName); + + const cfgObject = cfg.statements[0] && tryCast(cfg.statements[0].expression, isObjectLiteralExpression); + const filesProp = cfgObject && find(cfgObject.properties, (prop): prop is PropertyAssignment => + isPropertyAssignment(prop) && isStringLiteral(prop.name) && prop.name.text === "files"); + if (filesProp && isArrayLiteralExpression(filesProp.initializer)) { + changes.insertNodeInListAfter(cfg, last(filesProp.initializer.elements), factory.createStringLiteral(newFilePath), filesProp.initializer.elements); + } +} + +/** @internal */ +export function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { + for (const { first, afterLast } of moved) { + changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); + } +} + +/** @internal */ +export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: Set, checker: TypeChecker) { + for (const statement of oldFile.statements) { + if (contains(toMove, statement)) continue; + forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)!))); + } +} + +/** @internal */ +export function updateImportsInOtherFiles( + changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: Set, targetFileName: string, quotePreference: QuotePreference +): void { + const checker = program.getTypeChecker(); + for (const sourceFile of program.getSourceFiles()) { + if (sourceFile === oldFile) continue; + for (const statement of sourceFile.statements) { + forEachImportInStatement(statement, importNode => { + if (checker.getSymbolAtLocation(moduleSpecifierFromImport(importNode)) !== oldFile.symbol) return; + + const shouldMove = (name: Identifier): boolean => { + const symbol = isBindingElement(name.parent) + ? getPropertySymbolFromBindingElement(checker, name.parent as ObjectBindingElementWithoutPropertyName) + : skipAlias(checker.getSymbolAtLocation(name)!, checker); + return !!symbol && movedSymbols.has(symbol); + }; + deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file + + const pathToTargetFileWithExtension = resolvePath(getDirectoryPath(oldFile.path), targetFileName); + const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToTargetFileWithExtension, createModuleSpecifierResolutionHost(program, host)); + const newImportDeclaration = filterImport(importNode, makeStringLiteral(newModuleSpecifier, quotePreference), shouldMove); + if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); + + const ns = getNamespaceLikeImport(importNode); + if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleSpecifier, ns, importNode, quotePreference); + }); + } + } +} + +function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ? + node.importClause.namedBindings.name : undefined; + case SyntaxKind.ImportEqualsDeclaration: + return node.name; + case SyntaxKind.VariableDeclaration: + return tryCast(node.name, isIdentifier); + default: + return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); + } +} + +function updateNamespaceLikeImport( + changes: textChanges.ChangeTracker, + sourceFile: SourceFile, + checker: TypeChecker, + movedSymbols: Set, + newModuleSpecifier: string, + oldImportId: Identifier, + oldImportNode: SupportedImport, + quotePreference: QuotePreference +): void { + const preferredNewNamespaceName = codefix.moduleSpecifierToValidIdentifier(newModuleSpecifier, ScriptTarget.ESNext); + let needUniqueName = false; + const toChange: Identifier[] = []; + FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => { + if (!isPropertyAccessExpression(ref.parent)) return; + needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, SymbolFlags.All, /*excludeGlobals*/ true); + if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) { + toChange.push(ref); + } + }); + + if (toChange.length) { + const newNamespaceName = needUniqueName ? getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName; + for (const ref of toChange) { + changes.replaceNode(sourceFile, ref, factory.createIdentifier(newNamespaceName)); + } + changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, preferredNewNamespaceName, newModuleSpecifier, quotePreference)); + } +} + +function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string, quotePreference: QuotePreference): Node { + const newNamespaceId = factory.createIdentifier(newNamespaceName); + const newModuleString = makeStringLiteral(newModuleSpecifier, quotePreference); + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + return factory.createImportDeclaration( + /*modifiers*/ undefined, + factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, factory.createNamespaceImport(newNamespaceId)), + newModuleString, + /*assertClause*/ undefined); + case SyntaxKind.ImportEqualsDeclaration: + return factory.createImportEqualsDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, newNamespaceId, factory.createExternalModuleReference(newModuleString)); + case SyntaxKind.VariableDeclaration: + return factory.createVariableDeclaration(newNamespaceId, /*exclamationToken*/ undefined, /*type*/ undefined, createRequireCall(newModuleString)); + default: + return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); + } +} + +function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { + return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); +} + +/** @internal */ +export function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { + return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier + : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression + : i.initializer.arguments[0]); +} + +/** @internal */ +export function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void { + if (isImportDeclaration(statement)) { + if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport); + } + else if (isImportEqualsDeclaration(statement)) { + if (isExternalModuleReference(statement.moduleReference) && isStringLiteralLike(statement.moduleReference.expression)) { + cb(statement as SupportedImport); + } + } + else if (isVariableStatement(statement)) { + for (const decl of statement.declarationList.declarations) { + if (decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true)) { + cb(decl as SupportedImport); + } + } + } +} + +/** @internal */ +export type SupportedImport = + | ImportDeclaration & { moduleSpecifier: StringLiteralLike } + | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } } + | VariableDeclaration & { initializer: RequireOrImportCall }; + +/** @internal */ +export type SupportedImportStatement = + | ImportDeclaration + | ImportEqualsDeclaration + | VariableStatement; + +/** @internal */ +export function createOldFileImportsFromTargetFile( + sourceFile: SourceFile, + targetFileNeedExport: Set, + targetFileNameWithExtension: string, + program: Program, + host: LanguageServiceHost, + useEs6Imports: boolean, + quotePreference: QuotePreference +): AnyImportOrRequireStatement | undefined { + let defaultImport: Identifier | undefined; + const imports: string[] = []; + targetFileNeedExport.forEach(symbol => { + if (symbol.escapedName === InternalSymbolName.Default) { + defaultImport = factory.createIdentifier(symbolNameNoDefault(symbol)!); + } + else { + imports.push(symbol.name); + } + }); + return makeImportOrRequire(sourceFile, defaultImport, imports, targetFileNameWithExtension, program, host, useEs6Imports, quotePreference); +} + +/** @internal */ +export function makeImportOrRequire( + sourceFile: SourceFile, + defaultImport: Identifier | undefined, + imports: readonly string[], + targetFileNameWithExtension: string, + program: Program, + host: LanguageServiceHost, + useEs6Imports: boolean, + quotePreference: QuotePreference +): AnyImportOrRequireStatement | undefined { + const pathToTargetFile = resolvePath(getDirectoryPath(sourceFile.path), targetFileNameWithExtension); + const pathToTargetFileWithCorrectExtension = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToTargetFile, createModuleSpecifierResolutionHost(program, host)); + + if (useEs6Imports) { + const specifiers = imports.map(i => factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, factory.createIdentifier(i))); + return makeImportIfNecessary(defaultImport, specifiers, pathToTargetFileWithCorrectExtension, quotePreference); + } + else { + Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module. + const bindingElements = imports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i)); + return bindingElements.length + ? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(makeStringLiteral(pathToTargetFileWithCorrectExtension, quotePreference))) as RequireVariableStatement + : undefined; + } +} + +function makeVariableStatement(name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined, flags: NodeFlags = NodeFlags.Const) { + return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags)); +} + +/** @internal */ +export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: Set, useEs6Exports: boolean): readonly Statement[] { + return flatMap(toMove, statement => { + if (isTopLevelDeclarationStatement(statement) && + !isExported(sourceFile, statement, useEs6Exports) && + forEachTopLevelDeclaration(statement, d => needExport.has(Debug.checkDefined(tryCast(d, canHaveSymbol)?.symbol)))) { + const exports = addExport(getSynthesizedDeepClone(statement), useEs6Exports); + if (exports) return exports; + } + return getSynthesizedDeepClone(statement); + }); +} + +function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: Identifier): boolean { + if (useEs6Exports) { + return !isExpressionStatement(decl) && hasSyntacticModifier(decl, ModifierFlags.Export) || !!(name && sourceFile.symbol && sourceFile.symbol.exports?.has(name.escapedText)); + } + return !!sourceFile.symbol && !!sourceFile.symbol.exports && + getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name))); +} + +/** @internal */ +export function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { + switch (importDecl.kind) { + case SyntaxKind.ImportDeclaration: + deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused); + break; + case SyntaxKind.ImportEqualsDeclaration: + if (isUnused(importDecl.name)) { + changes.delete(sourceFile, importDecl); + } + break; + case SyntaxKind.VariableDeclaration: + deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused); + break; + default: + Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`); + } +} + +function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { + if (!importDecl.importClause) return; + const { name, namedBindings } = importDecl.importClause; + const defaultUnused = !name || isUnused(name); + const namedBindingsUnused = !namedBindings || + (namedBindings.kind === SyntaxKind.NamespaceImport ? isUnused(namedBindings.name) : namedBindings.elements.length !== 0 && namedBindings.elements.every(e => isUnused(e.name))); + if (defaultUnused && namedBindingsUnused) { + changes.delete(sourceFile, importDecl); + } + else { + if (name && defaultUnused) { + changes.delete(sourceFile, name); + } + if (namedBindings) { + if (namedBindingsUnused) { + changes.replaceNode( + sourceFile, + importDecl.importClause, + factory.updateImportClause(importDecl.importClause, importDecl.importClause.isTypeOnly, name, /*namedBindings*/ undefined) + ); + } + else if (namedBindings.kind === SyntaxKind.NamedImports) { + for (const element of namedBindings.elements) { + if (isUnused(element.name)) changes.delete(sourceFile, element); + } + } + } + } +} + +function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean) { + const { name } = varDecl; + switch (name.kind) { + case SyntaxKind.Identifier: + if (isUnused(name)) { + if (varDecl.initializer && isRequireCall(varDecl.initializer, /*requireStringLiteralLikeArgument*/ true)) { + changes.delete(sourceFile, + isVariableDeclarationList(varDecl.parent) && length(varDecl.parent.declarations) === 1 ? varDecl.parent.parent : varDecl); + } + else { + changes.delete(sourceFile, name); + } + } + break; + case SyntaxKind.ArrayBindingPattern: + break; + case SyntaxKind.ObjectBindingPattern: + if (name.elements.every(e => isIdentifier(e.name) && isUnused(e.name))) { + changes.delete(sourceFile, + isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl); + } + else { + for (const element of name.elements) { + if (isIdentifier(element.name) && isUnused(element.name)) { + changes.delete(sourceFile, element.name); + } + } + } + break; + } +} + +/** @internal */ +export type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; + +function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { + Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); + return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); +} + +function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly Statement[] | undefined { + return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl); +} + +function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement { + const modifiers = canHaveModifiers(d) ? concatenate([factory.createModifier(SyntaxKind.ExportKeyword)], getModifiers(d)) : undefined; + switch (d.kind) { + case SyntaxKind.FunctionDeclaration: + return factory.updateFunctionDeclaration(d, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body); + case SyntaxKind.ClassDeclaration: + const decorators = canHaveDecorators(d) ? getDecorators(d) : undefined; + return factory.updateClassDeclaration(d, concatenate(decorators, modifiers), d.name, d.typeParameters, d.heritageClauses, d.members); + case SyntaxKind.VariableStatement: + return factory.updateVariableStatement(d, modifiers, d.declarationList); + case SyntaxKind.ModuleDeclaration: + return factory.updateModuleDeclaration(d, modifiers, d.name, d.body); + case SyntaxKind.EnumDeclaration: + return factory.updateEnumDeclaration(d, modifiers, d.name, d.members); + case SyntaxKind.TypeAliasDeclaration: + return factory.updateTypeAliasDeclaration(d, modifiers, d.name, d.typeParameters, d.type); + case SyntaxKind.InterfaceDeclaration: + return factory.updateInterfaceDeclaration(d, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); + case SyntaxKind.ImportEqualsDeclaration: + return factory.updateImportEqualsDeclaration(d, modifiers, d.isTypeOnly, d.name, d.moduleReference); + case SyntaxKind.ExpressionStatement: + // Shouldn't try to add 'export' keyword to `exports.x = ...` + return Debug.fail(); + default: + return Debug.assertNever(d, `Unexpected declaration kind ${(d as DeclarationStatement).kind}`); + } +} +function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly Statement[] | undefined { + return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)]; +} + +/** Creates `exports.x = x;` */ +function createExportAssignment(name: string): Statement { + return factory.createExpressionStatement( + factory.createBinaryExpression( + factory.createPropertyAccessExpression(factory.createIdentifier("exports"), factory.createIdentifier(name)), + SyntaxKind.EqualsToken, + factory.createIdentifier(name))); +} + +function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] { + switch (decl.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + return [decl.name!.text]; // TODO: GH#18217 + case SyntaxKind.VariableStatement: + return mapDefined(decl.declarationList.declarations, d => isIdentifier(d.name) ? d.name.text : undefined); + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return emptyArray; + case SyntaxKind.ExpressionStatement: + // Shouldn't try to add 'export' keyword to `exports.x = ...` + return Debug.fail("Can't export an ExpressionStatement"); + default: + return Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`); + } +} + +/** @internal */ +export function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { + switch (i.kind) { + case SyntaxKind.ImportDeclaration: { + const clause = i.importClause; + if (!clause) return undefined; + const defaultImport = clause.name && keep(clause.name) ? clause.name : undefined; + const namedBindings = clause.namedBindings && filterNamedBindings(clause.namedBindings, keep); + return defaultImport || namedBindings + ? factory.createImportDeclaration(/*modifiers*/ undefined, factory.createImportClause(clause.isTypeOnly, defaultImport, namedBindings), getSynthesizedDeepClone(moduleSpecifier), /*assertClause*/ undefined) + : undefined; + } + case SyntaxKind.ImportEqualsDeclaration: + return keep(i.name) ? i : undefined; + case SyntaxKind.VariableDeclaration: { + const name = filterBindingName(i.name, keep); + return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined; + } + default: + return Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`); + } +} + +function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined { + if (namedBindings.kind === SyntaxKind.NamespaceImport) { + return keep(namedBindings.name) ? namedBindings : undefined; + } + else { + const newElements = namedBindings.elements.filter(e => keep(e.name)); + return newElements.length ? factory.createNamedImports(newElements) : undefined; + } +} + +function filterBindingName(name: BindingName, keep: (name: Identifier) => boolean): BindingName | undefined { + switch (name.kind) { + case SyntaxKind.Identifier: + return keep(name) ? name : undefined; + case SyntaxKind.ArrayBindingPattern: + return name; + case SyntaxKind.ObjectBindingPattern: { + // We can't handle nested destructurings or property names well here, so just copy them all. + const newElements = name.elements.filter(prop => prop.propertyName || !isIdentifier(prop.name) || keep(prop.name)); + return newElements.length ? factory.createObjectBindingPattern(newElements) : undefined; + } + } +} + +/** @internal */ +export function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { + return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); +} + +/** @internal */ +export function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { + switch (d.kind) { + case SyntaxKind.VariableDeclaration: + return d.parent.parent; + case SyntaxKind.BindingElement: + return getTopLevelDeclarationStatement( + cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(p) || isBindingElement(p))); + default: + return d; + } +} + +/** @internal */ +export function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, name: Identifier, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void { + if (isExported(sourceFile, decl, useEs6Exports, name)) return; + if (useEs6Exports) { + if (!isExpressionStatement(decl)) changes.insertExportModifier(sourceFile, decl); + } + else { + const names = getNamesToExportInCommonJS(decl); + if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); + } +} + +/** @internal */ +export interface ToMove { + readonly all: readonly Statement[]; + readonly ranges: readonly StatementRange[]; +} + +/** @internal */ +export interface StatementRange { + readonly first: Statement; + readonly afterLast: Statement | undefined; +} + +/** @internal */ +export interface UsageInfo { + // Symbols whose declarations are moved from the old file to the new file. + readonly movedSymbols: Set; + + // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) + readonly targetFileImportsFromOldFile: Set; + // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. + readonly oldFileImportsFromTargetFile: Set; + + readonly oldImportsNeededByTargetFile: Map; + // Subset of oldImportsNeededByTargetFile that are will no longer be used in the old file. + readonly unusedImportsFromOldFile: Set; +} + +/** @internal */ +export type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' + +/** @internal */ +export type NonVariableTopLevelDeclaration = + | FunctionDeclaration + | ClassDeclaration + | EnumDeclaration + | TypeAliasDeclaration + | InterfaceDeclaration + | ModuleDeclaration + | TopLevelExpressionStatement + | ImportEqualsDeclaration; + + /** @internal */ +export interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } + +/** @internal */ +export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; + +/** @internal */ +export function createNewFileName(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { + const checker = program.getTypeChecker(); + const toMove = getStatementsToMove(context); + let usage; + if (toMove) { + usage = getUsageInfo(oldFile, toMove.all, checker); + const currentDirectory = getDirectoryPath(oldFile.fileName); + const extension = extensionFromPath(oldFile.fileName); + const newFileName = combinePaths( + // new file is always placed in the same directory as the old file + currentDirectory, + // ensures the filename computed below isn't already taken + makeUniqueFilename( + // infers a name for the new file from the symbols being moved + inferNewFileName(usage.oldFileImportsFromTargetFile, usage.movedSymbols), + extension, + currentDirectory, + host)) + // new file has same extension as old file + + extension; + return newFileName; + } + return ""; +} + +interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } + +function getRangeToMove(context: RefactorContext): RangeToMove | undefined { + const { file } = context; + const range = createTextRangeFromSpan(getRefactorContextSpan(context)); + const { statements } = file; + + const startNodeIndex = findIndex(statements, s => s.end > range.pos); + if (startNodeIndex === -1) return undefined; + + const startStatement = statements[startNodeIndex]; + if (isNamedDeclaration(startStatement) && startStatement.name && rangeContainsRange(startStatement.name, range)) { + return { toMove: [statements[startNodeIndex]], afterLast: statements[startNodeIndex + 1] }; + } + + // Can't only partially include the start node or be partially into the next node + if (range.pos > startStatement.getStart(file)) return undefined; + const afterEndNodeIndex = findIndex(statements, s => s.end > range.end, startNodeIndex); + // Can't be partially into the next node + if (afterEndNodeIndex !== -1 && (afterEndNodeIndex === 0 || statements[afterEndNodeIndex].getStart(file) < range.end)) return undefined; + + return { + toMove: statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex), + afterLast: afterEndNodeIndex === -1 ? undefined : statements[afterEndNodeIndex], + }; +} + +/** @internal */ +export function getStatementsToMove(context: RefactorContext): ToMove | undefined { + const rangeToMove = getRangeToMove(context); + if (rangeToMove === undefined) return undefined; + const all: Statement[] = []; + const ranges: StatementRange[] = []; + const { toMove, afterLast } = rangeToMove; + getRangesWhere(toMove, isAllowedStatementToMove, (start, afterEndIndex) => { + for (let i = start; i < afterEndIndex; i++) all.push(toMove[i]); + ranges.push({ first: toMove[start], afterLast }); + }); + return all.length === 0 ? undefined : { all, ranges }; +} + +function isAllowedStatementToMove(statement: Statement): boolean { + // Filters imports and prologue directives out of the range of statements to move. + // Imports will be copied to the new file anyway, and may still be needed in the old file. + // Prologue directives will be copied to the new file and should be left in the old file. + return !isPureImport(statement) && !isPrologueDirective(statement); +} + +function isPureImport(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + return true; + case SyntaxKind.ImportEqualsDeclaration: + return !hasSyntacticModifier(node, ModifierFlags.Export); + case SyntaxKind.VariableStatement: + return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*requireStringLiteralLikeArgument*/ true)); + default: + return false; + } +} + +/** @internal */ +export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { + const movedSymbols = new Set(); + const oldImportsNeededByTargetFile = new Map(); + const targetFileImportsFromOldFile = new Set(); + + const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx)); + const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx); + + if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code) + oldImportsNeededByTargetFile.set(jsxNamespaceSymbol, false); + } + + for (const statement of toMove) { + forEachTopLevelDeclaration(statement, decl => { + movedSymbols.add(Debug.checkDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol, "Need a symbol here")); + }); + } + for (const statement of toMove) { + forEachReference(statement, checker, (symbol, isValidTypeOnlyUseSite) => { + if (!symbol.declarations) return; + for (const decl of symbol.declarations) { + if (isInImport(decl)) { + const prevIsTypeOnly = oldImportsNeededByTargetFile.get(symbol); + oldImportsNeededByTargetFile.set(symbol, prevIsTypeOnly === undefined ? isValidTypeOnlyUseSite : prevIsTypeOnly && isValidTypeOnlyUseSite); + } + else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { + targetFileImportsFromOldFile.add(symbol); + } + } + }); + } + const unusedImportsFromOldFile = new Set(oldImportsNeededByTargetFile.keys()); + + const oldFileImportsFromTargetFile = new Set(); + for (const statement of oldFile.statements) { + if (contains(toMove, statement)) continue; + + // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByTargetFile. + if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) { + unusedImportsFromOldFile.delete(jsxNamespaceSymbol); + } + + forEachReference(statement, checker, symbol => { + if (movedSymbols.has(symbol)) oldFileImportsFromTargetFile.add(symbol); + unusedImportsFromOldFile.delete(symbol); + }); + } + + return { movedSymbols, targetFileImportsFromOldFile, oldFileImportsFromTargetFile, oldImportsNeededByTargetFile, unusedImportsFromOldFile }; + + function getJsxNamespaceSymbol(containsJsx: Node | undefined) { + if (containsJsx === undefined) { + return undefined; + } + + const jsxNamespace = checker.getJsxNamespace(containsJsx); + + // Strictly speaking, this could resolve to a symbol other than the JSX namespace. + // This will produce erroneous output (probably, an incorrectly copied import) but + // is expected to be very rare and easily reversible. + const jsxNamespaceSymbol = checker.resolveName(jsxNamespace, containsJsx, SymbolFlags.Namespace, /*excludeGlobals*/ true); + + return !!jsxNamespaceSymbol && some(jsxNamespaceSymbol.declarations, isInImport) + ? jsxNamespaceSymbol + : undefined; + } +} + +function makeUniqueFilename(proposedFilename: string, extension: string, inDirectory: string, host: LanguageServiceHost): string { + let newFilename = proposedFilename; + for (let i = 1; ; i++) { + const name = combinePaths(inDirectory, newFilename + extension); + if (!host.fileExists(name)) return newFilename; + newFilename = `${proposedFilename}.${i}`; + } +} + +function inferNewFileName(importsFromNewFile: Set, movedSymbols: Set): string { + return forEachKey(importsFromNewFile, symbolNameNoDefault) || forEachKey(movedSymbols, symbolNameNoDefault) || "newFile"; +} + +function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol, isValidTypeOnlyUseSite: boolean) => void) { + node.forEachChild(function cb(node) { + if (isIdentifier(node) && !isDeclarationName(node)) { + const sym = checker.getSymbolAtLocation(node); + if (sym) onReference(sym, isValidTypeOnlyAliasUseSite(node)); + } + else { + node.forEachChild(cb); + } + }); +} + +function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { + switch (statement.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return cb(statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ImportEqualsDeclaration); + + case SyntaxKind.VariableStatement: + return firstDefined((statement as VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb)); + + case SyntaxKind.ExpressionStatement: { + const { expression } = statement as ExpressionStatement; + return isBinaryExpression(expression) && getAssignmentDeclarationKind(expression) === AssignmentDeclarationKind.ExportsProperty + ? cb(statement as TopLevelExpressionStatement) + : undefined; + } + } +} + +function isInImport(decl: Declaration) { + switch (decl.kind) { + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ImportClause: + case SyntaxKind.NamespaceImport: + return true; + case SyntaxKind.VariableDeclaration: + return isVariableDeclarationInImport(decl as VariableDeclaration); + case SyntaxKind.BindingElement: + return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent); + default: + return false; + } +} + +function isVariableDeclarationInImport(decl: VariableDeclaration) { + return isSourceFile(decl.parent.parent.parent) && + !!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true); +} + +/** @internal */ +export function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { + return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent); +} +function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): Node { + return isVariableDeclaration(node) ? node.parent.parent.parent : node.parent; +} + +function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined { + switch (name.kind) { + case SyntaxKind.Identifier: + return cb(cast(name.parent, (x): x is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(x) || isBindingElement(x))); + case SyntaxKind.ArrayBindingPattern: + case SyntaxKind.ObjectBindingPattern: + return firstDefined(name.elements, em => isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb)); + default: + return Debug.assertNever(name, `Unexpected name kind ${(name as BindingName).kind}`); + } +} + +function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return true; + default: + return false; + } +} + + diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 2fdff08a85282..10183ad20acc4 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -1,135 +1,56 @@ -import { getModuleSpecifier } from "../../compiler/moduleSpecifiers"; import { - AnyImportOrRequireStatement, append, ApplicableRefactorInfo, - AssignmentDeclarationKind, - BinaryExpression, - BindingElement, - BindingName, - CallExpression, - canHaveDecorators, - canHaveModifiers, - canHaveSymbol, cast, - ClassDeclaration, - codefix, - combinePaths, - concatenate, - contains, - copyEntries, - createModuleSpecifierResolutionHost, - createTextRangeFromSpan, Debug, - Declaration, - DeclarationStatement, Diagnostics, emptyArray, - EnumDeclaration, - escapeLeadingUnderscores, - Expression, - ExpressionStatement, - extensionFromPath, - ExternalModuleReference, - factory, - find, - FindAllReferences, - findIndex, - firstDefined, - flatMap, - forEachEntry, - FunctionDeclaration, - getAssignmentDeclarationKind, + fileShouldUseJavaScriptRequire, getBaseFileName, - GetCanonicalFileName, - getDecorators, - getDirectoryPath, getLocaleSpecificMessage, - getModifiers, - getPropertySymbolFromBindingElement, getQuotePreference, - getRangesWhere, - getRefactorContextSpan, - getRelativePathFromFile, - getSymbolId, - getUniqueName, hasSyntacticModifier, hostGetCanonicalFileName, Identifier, - ImportDeclaration, - ImportEqualsDeclaration, insertImports, - InterfaceDeclaration, - InternalSymbolName, - isArrayLiteralExpression, - isBinaryExpression, - isBindingElement, - isDeclarationName, - isExpressionStatement, - isExternalModuleReference, - isIdentifier, - isImportDeclaration, - isImportEqualsDeclaration, - isNamedDeclaration, - isObjectLiteralExpression, - isOmittedExpression, isPrologueDirective, - isPropertyAccessExpression, - isPropertyAssignment, - isRequireCall, - isSourceFile, - isStringLiteral, - isStringLiteralLike, - isVariableDeclaration, - isVariableDeclarationList, - isVariableStatement, LanguageServiceHost, - last, - length, - makeImportIfNecessary, - makeStringLiteral, - mapDefined, ModifierFlags, - ModifierLike, - ModuleDeclaration, - NamedImportBindings, - Node, - NodeFlags, nodeSeenTracker, - normalizePath, - ObjectBindingElementWithoutPropertyName, Program, - PropertyAccessExpression, - PropertyAssignment, QuotePreference, - rangeContainsRange, RefactorContext, RefactorEditInfo, - RequireOrImportCall, - RequireVariableStatement, - resolvePath, - ScriptTarget, - skipAlias, - some, SourceFile, - Statement, - StringLiteralLike, Symbol, - SymbolFlags, - symbolNameNoDefault, SyntaxKind, takeWhile, textChanges, - TransformFlags, - tryCast, - TypeAliasDeclaration, TypeChecker, - TypeNode, UserPreferences, - VariableDeclaration, - VariableDeclarationList, - VariableStatement, } from "../_namespaces/ts"; -import { registerRefactor } from "../_namespaces/ts.refactor"; +import { + addExports, + addExportToChanges, + addNewFileToTsconfig, + createNewFileName, + createOldFileImportsFromTargetFile, + deleteMovedStatements, + deleteUnusedOldImports, + filterImport, + forEachImportInStatement, + getStatementsToMove, + getTopLevelDeclarationStatement, + getUsageInfo, + isTopLevelDeclaration, + makeImportOrRequire, + moduleSpecifierFromImport, + nameOfTopLevelDeclaration, + registerRefactor, + SupportedImportStatement, + ToMove, + updateImportsInOtherFiles, + UsageInfo +} from "../_namespaces/ts.refactor"; const refactorName = "Move to a new file"; const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); @@ -156,55 +77,16 @@ registerRefactor(refactorName, { getEditsForAction: function getRefactorEditsToMoveToNewFile(context, actionName): RefactorEditInfo { Debug.assert(actionName === refactorName, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences)); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences, context)); return { edits, renameFilename: undefined, renameLocation: undefined }; } }); -interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } -function getRangeToMove(context: RefactorContext): RangeToMove | undefined { - const { file } = context; - const range = createTextRangeFromSpan(getRefactorContextSpan(context)); - const { statements } = file; - - const startNodeIndex = findIndex(statements, s => s.end > range.pos); - if (startNodeIndex === -1) return undefined; - - const startStatement = statements[startNodeIndex]; - if (isNamedDeclaration(startStatement) && startStatement.name && rangeContainsRange(startStatement.name, range)) { - return { toMove: [statements[startNodeIndex]], afterLast: statements[startNodeIndex + 1] }; - } - - // Can't only partially include the start node or be partially into the next node - if (range.pos > startStatement.getStart(file)) return undefined; - const afterEndNodeIndex = findIndex(statements, s => s.end > range.end, startNodeIndex); - // Can't be partially into the next node - if (afterEndNodeIndex !== -1 && (afterEndNodeIndex === 0 || statements[afterEndNodeIndex].getStart(file) < range.end)) return undefined; - - return { - toMove: statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex), - afterLast: afterEndNodeIndex === -1 ? undefined : statements[afterEndNodeIndex], - }; -} - -function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { +function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences, context: RefactorContext): void { const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); - const currentDirectory = getDirectoryPath(oldFile.fileName); - const extension = extensionFromPath(oldFile.fileName); - const newFilename = combinePaths( - // new file is always placed in the same directory as the old file - currentDirectory, - // ensures the filename computed below isn't already taken - makeUniqueFilename( - // infers a name for the new file from the symbols being moved - inferNewFilename(usage.oldFileImportsFromNewFile, usage.movedSymbols), - extension, - currentDirectory, - host)) - // new file has same extension as old file - + extension; + const newFilename = createNewFileName(oldFile, program, context, host); // If previous file was global, this is easy. changes.createNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFilename, preferences)); @@ -212,76 +94,19 @@ function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes addNewFileToTsconfig(program, changes, oldFile.fileName, newFilename, hostGetCanonicalFileName(host)); } -interface StatementRange { - readonly first: Statement; - readonly afterLast: Statement | undefined; -} -interface ToMove { - readonly all: readonly Statement[]; - readonly ranges: readonly StatementRange[]; -} - -function getStatementsToMove(context: RefactorContext): ToMove | undefined { - const rangeToMove = getRangeToMove(context); - if (rangeToMove === undefined) return undefined; - const all: Statement[] = []; - const ranges: StatementRange[] = []; - const { toMove, afterLast } = rangeToMove; - getRangesWhere(toMove, isAllowedStatementToMove, (start, afterEndIndex) => { - for (let i = start; i < afterEndIndex; i++) all.push(toMove[i]); - ranges.push({ first: toMove[start], afterLast }); - }); - return all.length === 0 ? undefined : { all, ranges }; -} - -function isAllowedStatementToMove(statement: Statement): boolean { - // Filters imports and prologue directives out of the range of statements to move. - // Imports will be copied to the new file anyway, and may still be needed in the old file. - // Prologue directives will be copied to the new file and should be left in the old file. - return !isPureImport(statement) && !isPrologueDirective(statement); -} - -function isPureImport(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.ImportDeclaration: - return true; - case SyntaxKind.ImportEqualsDeclaration: - return !hasSyntacticModifier(node, ModifierFlags.Export); - case SyntaxKind.VariableStatement: - return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*requireStringLiteralLikeArgument*/ true)); - default: - return false; - } -} - -function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { - const cfg = program.getCompilerOptions().configFile; - if (!cfg) return; - - const newFileAbsolutePath = normalizePath(combinePaths(oldFileName, "..", newFileNameWithExtension)); - const newFilePath = getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName); - - const cfgObject = cfg.statements[0] && tryCast(cfg.statements[0].expression, isObjectLiteralExpression); - const filesProp = cfgObject && find(cfgObject.properties, (prop): prop is PropertyAssignment => - isPropertyAssignment(prop) && isStringLiteral(prop.name) && prop.name.text === "files"); - if (filesProp && isArrayLiteralExpression(filesProp.initializer)) { - changes.insertNodeInListAfter(cfg, last(filesProp.initializer.elements), factory.createStringLiteral(newFilePath), filesProp.initializer.elements); - } -} - function getNewStatementsAndRemoveFromOldFile( oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, ) { const checker = program.getTypeChecker(); const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); - if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByNewFile.size() === 0) { + if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByTargetFile.size === 0) { deleteMovedStatements(oldFile, toMove.ranges, changes); return [...prologueDirectives, ...toMove.all]; } - const useEsModuleSyntax = !!oldFile.externalModuleIndicator; + const useEsModuleSyntax = !fileShouldUseJavaScriptRequire(newFilename, program, host, !!oldFile.commonJsModuleIndicator); const quotePreference = getQuotePreference(oldFile, preferences); - const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); + const importsFromNewFile = createOldFileImportsFromTargetFile(oldFile, usage.oldFileImportsFromTargetFile, newFilename, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); } @@ -290,8 +115,8 @@ function getNewStatementsAndRemoveFromOldFile( deleteMovedStatements(oldFile, toMove.ranges, changes); updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename, quotePreference); - const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference); - const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); + const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByTargetFile, usage.targetFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference); + const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromTargetFile, useEsModuleSyntax); if (imports.length && body.length) { return [ ...prologueDirectives, @@ -308,295 +133,10 @@ function getNewStatementsAndRemoveFromOldFile( ]; } -function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { - for (const { first, afterLast } of moved) { - changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); - } -} - -function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) { - for (const statement of oldFile.statements) { - if (contains(toMove, statement)) continue; - forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)!))); - } -} - -function updateImportsInOtherFiles( - changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newFilename: string, quotePreference: QuotePreference -): void { - const checker = program.getTypeChecker(); - for (const sourceFile of program.getSourceFiles()) { - if (sourceFile === oldFile) continue; - for (const statement of sourceFile.statements) { - forEachImportInStatement(statement, importNode => { - if (checker.getSymbolAtLocation(moduleSpecifierFromImport(importNode)) !== oldFile.symbol) return; - - const shouldMove = (name: Identifier): boolean => { - const symbol = isBindingElement(name.parent) - ? getPropertySymbolFromBindingElement(checker, name.parent as ObjectBindingElementWithoutPropertyName) - : skipAlias(checker.getSymbolAtLocation(name)!, checker); // TODO: GH#18217 - return !!symbol && movedSymbols.has(symbol); - }; - deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file - - const pathToNewFileWithExtension = resolvePath(getDirectoryPath(oldFile.path), newFilename); - const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFileWithExtension, createModuleSpecifierResolutionHost(program, host)); - const newImportDeclaration = filterImport(importNode, makeStringLiteral(newModuleSpecifier, quotePreference), shouldMove); - if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); - - const ns = getNamespaceLikeImport(importNode); - if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleSpecifier, ns, importNode, quotePreference); - }); - } - } -} - -function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { - switch (node.kind) { - case SyntaxKind.ImportDeclaration: - return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ? - node.importClause.namedBindings.name : undefined; - case SyntaxKind.ImportEqualsDeclaration: - return node.name; - case SyntaxKind.VariableDeclaration: - return tryCast(node.name, isIdentifier); - default: - return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); - } -} - -function updateNamespaceLikeImport( - changes: textChanges.ChangeTracker, - sourceFile: SourceFile, - checker: TypeChecker, - movedSymbols: ReadonlySymbolSet, - newModuleSpecifier: string, - oldImportId: Identifier, - oldImportNode: SupportedImport, - quotePreference: QuotePreference -): void { - const preferredNewNamespaceName = codefix.moduleSpecifierToValidIdentifier(newModuleSpecifier, ScriptTarget.ESNext); - let needUniqueName = false; - const toChange: Identifier[] = []; - FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => { - if (!isPropertyAccessExpression(ref.parent)) return; - needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, SymbolFlags.All, /*excludeGlobals*/ true); - if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) { - toChange.push(ref); - } - }); - - if (toChange.length) { - const newNamespaceName = needUniqueName ? getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName; - for (const ref of toChange) { - changes.replaceNode(sourceFile, ref, factory.createIdentifier(newNamespaceName)); - } - changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, preferredNewNamespaceName, newModuleSpecifier, quotePreference)); - } -} - -function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string, quotePreference: QuotePreference): Node { - const newNamespaceId = factory.createIdentifier(newNamespaceName); - const newModuleString = makeStringLiteral(newModuleSpecifier, quotePreference); - switch (node.kind) { - case SyntaxKind.ImportDeclaration: - return factory.createImportDeclaration( - /*modifiers*/ undefined, - factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, factory.createNamespaceImport(newNamespaceId)), - newModuleString, - /*assertClause*/ undefined); - case SyntaxKind.ImportEqualsDeclaration: - return factory.createImportEqualsDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, newNamespaceId, factory.createExternalModuleReference(newModuleString)); - case SyntaxKind.VariableDeclaration: - return factory.createVariableDeclaration(newNamespaceId, /*exclamationToken*/ undefined, /*type*/ undefined, createRequireCall(newModuleString)); - default: - return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); - } -} - -function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { - return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier - : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression - : i.initializer.arguments[0]); -} - -function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void { - if (isImportDeclaration(statement)) { - if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport); - } - else if (isImportEqualsDeclaration(statement)) { - if (isExternalModuleReference(statement.moduleReference) && isStringLiteralLike(statement.moduleReference.expression)) { - cb(statement as SupportedImport); - } - } - else if (isVariableStatement(statement)) { - for (const decl of statement.declarationList.declarations) { - if (decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true)) { - cb(decl as SupportedImport); - } - } - } -} - -type SupportedImport = - | ImportDeclaration & { moduleSpecifier: StringLiteralLike } - | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } } - | VariableDeclaration & { initializer: RequireOrImportCall }; -type SupportedImportStatement = - | ImportDeclaration - | ImportEqualsDeclaration - | VariableStatement; - -function createOldFileImportsFromNewFile( - sourceFile: SourceFile, - newFileNeedExport: ReadonlySymbolSet, - newFileNameWithExtension: string, - program: Program, - host: LanguageServiceHost, - useEs6Imports: boolean, - quotePreference: QuotePreference -): AnyImportOrRequireStatement | undefined { - let defaultImport: Identifier | undefined; - const imports: string[] = []; - newFileNeedExport.forEach(symbol => { - if (symbol.escapedName === InternalSymbolName.Default) { - defaultImport = factory.createIdentifier(symbolNameNoDefault(symbol)!); // TODO: GH#18217 - } - else { - imports.push(symbol.name); - } - }); - return makeImportOrRequire(sourceFile, defaultImport, imports, newFileNameWithExtension, program, host, useEs6Imports, quotePreference); -} - -function makeImportOrRequire( - sourceFile: SourceFile, - defaultImport: Identifier | undefined, - imports: readonly string[], - newFileNameWithExtension: string, - program: Program, - host: LanguageServiceHost, - useEs6Imports: boolean, - quotePreference: QuotePreference -): AnyImportOrRequireStatement | undefined { - const pathToNewFile = resolvePath(getDirectoryPath(sourceFile.path), newFileNameWithExtension); - const pathToNewFileWithCorrectExtension = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFile, createModuleSpecifierResolutionHost(program, host)); - - if (useEs6Imports) { - const specifiers = imports.map(i => factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, factory.createIdentifier(i))); - return makeImportIfNecessary(defaultImport, specifiers, pathToNewFileWithCorrectExtension, quotePreference); - } - else { - Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module. - const bindingElements = imports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i)); - return bindingElements.length - ? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(makeStringLiteral(pathToNewFileWithCorrectExtension, quotePreference))) as RequireVariableStatement - : undefined; - } -} - -function makeVariableStatement(name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined, flags: NodeFlags = NodeFlags.Const) { - return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags)); -} - -function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { - return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); -} - -function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] { - return flatMap(toMove, statement => { - if (isTopLevelDeclarationStatement(statement) && - !isExported(sourceFile, statement, useEs6Exports) && - forEachTopLevelDeclaration(statement, d => needExport.has(Debug.checkDefined(tryCast(d, canHaveSymbol)?.symbol)))) { - const exports = addExport(statement, useEs6Exports); - if (exports) return exports; - } - return statement; - }); -} - -function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { - switch (importDecl.kind) { - case SyntaxKind.ImportDeclaration: - deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused); - break; - case SyntaxKind.ImportEqualsDeclaration: - if (isUnused(importDecl.name)) { - changes.delete(sourceFile, importDecl); - } - break; - case SyntaxKind.VariableDeclaration: - deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused); - break; - default: - Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`); - } -} -function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { - if (!importDecl.importClause) return; - const { name, namedBindings } = importDecl.importClause; - const defaultUnused = !name || isUnused(name); - const namedBindingsUnused = !namedBindings || - (namedBindings.kind === SyntaxKind.NamespaceImport ? isUnused(namedBindings.name) : namedBindings.elements.length !== 0 && namedBindings.elements.every(e => isUnused(e.name))); - if (defaultUnused && namedBindingsUnused) { - changes.delete(sourceFile, importDecl); - } - else { - if (name && defaultUnused) { - changes.delete(sourceFile, name); - } - if (namedBindings) { - if (namedBindingsUnused) { - changes.replaceNode( - sourceFile, - importDecl.importClause, - factory.updateImportClause(importDecl.importClause, importDecl.importClause.isTypeOnly, name, /*namedBindings*/ undefined) - ); - } - else if (namedBindings.kind === SyntaxKind.NamedImports) { - for (const element of namedBindings.elements) { - if (isUnused(element.name)) changes.delete(sourceFile, element); - } - } - } - } -} -function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean) { - const { name } = varDecl; - switch (name.kind) { - case SyntaxKind.Identifier: - if (isUnused(name)) { - if (varDecl.initializer && isRequireCall(varDecl.initializer, /*requireStringLiteralLikeArgument*/ true)) { - changes.delete(sourceFile, - isVariableDeclarationList(varDecl.parent) && length(varDecl.parent.declarations) === 1 ? varDecl.parent.parent : varDecl); - } - else { - changes.delete(sourceFile, name); - } - } - break; - case SyntaxKind.ArrayBindingPattern: - break; - case SyntaxKind.ObjectBindingPattern: - if (name.elements.every(e => isIdentifier(e.name) && isUnused(e.name))) { - changes.delete(sourceFile, - isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl); - } - else { - for (const element of name.elements) { - if (isIdentifier(element.name) && isUnused(element.name)) { - changes.delete(sourceFile, element.name); - } - } - } - break; - } -} - function getNewFileImportsAndAddExportInOldFile( oldFile: SourceFile, - importsToCopy: ReadonlySymbolSet, - newFileImportsFromOldFile: ReadonlySymbolSet, + importsToCopy: Map, + newFileImportsFromOldFile: Set, changes: textChanges.ChangeTracker, checker: TypeChecker, program: Program, @@ -640,379 +180,3 @@ function getNewFileImportsAndAddExportInOldFile( append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, getBaseFileName(oldFile.fileName), program, host, useEsModuleSyntax, quotePreference)); return copiedOldImports; } - -function makeUniqueFilename(proposedFilename: string, extension: string, inDirectory: string, host: LanguageServiceHost): string { - let newFilename = proposedFilename; - for (let i = 1; ; i++) { - const name = combinePaths(inDirectory, newFilename + extension); - if (!host.fileExists(name)) return newFilename; - newFilename = `${proposedFilename}.${i}`; - } -} - -function inferNewFilename(importsFromNewFile: ReadonlySymbolSet, movedSymbols: ReadonlySymbolSet): string { - return importsFromNewFile.forEachEntry(symbolNameNoDefault) || movedSymbols.forEachEntry(symbolNameNoDefault) || "newFile"; -} - -interface UsageInfo { - // Symbols whose declarations are moved from the old file to the new file. - readonly movedSymbols: ReadonlySymbolSet; - - // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) - readonly newFileImportsFromOldFile: ReadonlySymbolSet; - // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. - readonly oldFileImportsFromNewFile: ReadonlySymbolSet; - - readonly oldImportsNeededByNewFile: ReadonlySymbolSet; - // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. - readonly unusedImportsFromOldFile: ReadonlySymbolSet; -} -function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { - const movedSymbols = new SymbolSet(); - const oldImportsNeededByNewFile = new SymbolSet(); - const newFileImportsFromOldFile = new SymbolSet(); - - const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx)); - const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx); - if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code) - oldImportsNeededByNewFile.add(jsxNamespaceSymbol); - } - - for (const statement of toMove) { - forEachTopLevelDeclaration(statement, decl => { - movedSymbols.add(Debug.checkDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol, "Need a symbol here")); - }); - } - for (const statement of toMove) { - forEachReference(statement, checker, symbol => { - if (!symbol.declarations) return; - for (const decl of symbol.declarations) { - if (isInImport(decl)) { - oldImportsNeededByNewFile.add(symbol); - } - else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { - newFileImportsFromOldFile.add(symbol); - } - } - }); - } - - const unusedImportsFromOldFile = oldImportsNeededByNewFile.clone(); - - const oldFileImportsFromNewFile = new SymbolSet(); - for (const statement of oldFile.statements) { - if (contains(toMove, statement)) continue; - - // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile. - if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) { - unusedImportsFromOldFile.delete(jsxNamespaceSymbol); - } - - forEachReference(statement, checker, symbol => { - if (movedSymbols.has(symbol)) oldFileImportsFromNewFile.add(symbol); - unusedImportsFromOldFile.delete(symbol); - }); - } - - return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; - - function getJsxNamespaceSymbol(containsJsx: Node | undefined) { - if (containsJsx === undefined) { - return undefined; - } - - const jsxNamespace = checker.getJsxNamespace(containsJsx); - - // Strictly speaking, this could resolve to a symbol other than the JSX namespace. - // This will produce erroneous output (probably, an incorrectly copied import) but - // is expected to be very rare and easily reversible. - const jsxNamespaceSymbol = checker.resolveName(jsxNamespace, containsJsx, SymbolFlags.Namespace, /*excludeGlobals*/ true); - - return !!jsxNamespaceSymbol && some(jsxNamespaceSymbol.declarations, isInImport) - ? jsxNamespaceSymbol - : undefined; - } -} - -// Below should all be utilities - -function isInImport(decl: Declaration) { - switch (decl.kind) { - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ImportClause: - case SyntaxKind.NamespaceImport: - return true; - case SyntaxKind.VariableDeclaration: - return isVariableDeclarationInImport(decl as VariableDeclaration); - case SyntaxKind.BindingElement: - return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent); - default: - return false; - } -} -function isVariableDeclarationInImport(decl: VariableDeclaration) { - return isSourceFile(decl.parent.parent.parent) && - !!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true); -} - -function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { - switch (i.kind) { - case SyntaxKind.ImportDeclaration: { - const clause = i.importClause; - if (!clause) return undefined; - const defaultImport = clause.name && keep(clause.name) ? clause.name : undefined; - const namedBindings = clause.namedBindings && filterNamedBindings(clause.namedBindings, keep); - return defaultImport || namedBindings - ? factory.createImportDeclaration(/*modifiers*/ undefined, factory.createImportClause(clause.isTypeOnly, defaultImport, namedBindings), moduleSpecifier, /*assertClause*/ undefined) - : undefined; - } - case SyntaxKind.ImportEqualsDeclaration: - return keep(i.name) ? i : undefined; - case SyntaxKind.VariableDeclaration: { - const name = filterBindingName(i.name, keep); - return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined; - } - default: - return Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`); - } -} -function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined { - if (namedBindings.kind === SyntaxKind.NamespaceImport) { - return keep(namedBindings.name) ? namedBindings : undefined; - } - else { - const newElements = namedBindings.elements.filter(e => keep(e.name)); - return newElements.length ? factory.createNamedImports(newElements) : undefined; - } -} -function filterBindingName(name: BindingName, keep: (name: Identifier) => boolean): BindingName | undefined { - switch (name.kind) { - case SyntaxKind.Identifier: - return keep(name) ? name : undefined; - case SyntaxKind.ArrayBindingPattern: - return name; - case SyntaxKind.ObjectBindingPattern: { - // We can't handle nested destructurings or property names well here, so just copy them all. - const newElements = name.elements.filter(prop => prop.propertyName || !isIdentifier(prop.name) || keep(prop.name)); - return newElements.length ? factory.createObjectBindingPattern(newElements) : undefined; - } - } -} - -function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol) => void) { - node.forEachChild(function cb(node) { - if (isIdentifier(node) && !isDeclarationName(node)) { - const sym = checker.getSymbolAtLocation(node); - if (sym) onReference(sym); - } - else { - node.forEachChild(cb); - } - }); -} - -interface ReadonlySymbolSet { - size(): number; - has(symbol: Symbol): boolean; - forEach(cb: (symbol: Symbol) => void): void; - forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; -} - -class SymbolSet implements ReadonlySymbolSet { - private map = new Map(); - add(symbol: Symbol): void { - this.map.set(String(getSymbolId(symbol)), symbol); - } - has(symbol: Symbol): boolean { - return this.map.has(String(getSymbolId(symbol))); - } - delete(symbol: Symbol): void { - this.map.delete(String(getSymbolId(symbol))); - } - forEach(cb: (symbol: Symbol) => void): void { - this.map.forEach(cb); - } - forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined { - return forEachEntry(this.map, cb); - } - clone(): SymbolSet { - const clone = new SymbolSet(); - copyEntries(this.map, clone.map); - return clone; - } - size() { - return this.map.size; - } -} - -type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' -type NonVariableTopLevelDeclaration = - | FunctionDeclaration - | ClassDeclaration - | EnumDeclaration - | TypeAliasDeclaration - | InterfaceDeclaration - | ModuleDeclaration - | TopLevelExpressionStatement - | ImportEqualsDeclaration; -type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; -interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } -type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; -function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { - return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent); -} - -function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): Node { - return isVariableDeclaration(node) ? node.parent.parent.parent : node.parent; -} - -function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { - Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); - return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); -} - -function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - return true; - default: - return false; - } -} - -function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { - switch (statement.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - return cb(statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ImportEqualsDeclaration); - - case SyntaxKind.VariableStatement: - return firstDefined((statement as VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb)); - - case SyntaxKind.ExpressionStatement: { - const { expression } = statement as ExpressionStatement; - return isBinaryExpression(expression) && getAssignmentDeclarationKind(expression) === AssignmentDeclarationKind.ExportsProperty - ? cb(statement as TopLevelExpressionStatement) - : undefined; - } - } -} -function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined { - switch (name.kind) { - case SyntaxKind.Identifier: - return cb(cast(name.parent, (x): x is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(x) || isBindingElement(x))); - case SyntaxKind.ArrayBindingPattern: - case SyntaxKind.ObjectBindingPattern: - return firstDefined(name.elements, em => isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb)); - default: - return Debug.assertNever(name, `Unexpected name kind ${(name as BindingName).kind}`); - } -} - -function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { - return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); -} - -function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { - switch (d.kind) { - case SyntaxKind.VariableDeclaration: - return d.parent.parent; - case SyntaxKind.BindingElement: - return getTopLevelDeclarationStatement( - cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(p) || isBindingElement(p))); - default: - return d; - } -} - -function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, name: Identifier, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void { - if (isExported(sourceFile, decl, useEs6Exports, name)) return; - if (useEs6Exports) { - if (!isExpressionStatement(decl)) changes.insertExportModifier(sourceFile, decl); - } - else { - const names = getNamesToExportInCommonJS(decl); - if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); - } -} - -function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: Identifier): boolean { - if (useEs6Exports) { - return !isExpressionStatement(decl) && hasSyntacticModifier(decl, ModifierFlags.Export) || !!(name && sourceFile.symbol.exports?.has(name.escapedText)); - } - return !!sourceFile.symbol && !!sourceFile.symbol.exports && - getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name))); -} - -function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly Statement[] | undefined { - return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl); -} -function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement { - const modifiers = canHaveModifiers(d) ? concatenate([factory.createModifier(SyntaxKind.ExportKeyword)], getModifiers(d)) : undefined; - switch (d.kind) { - case SyntaxKind.FunctionDeclaration: - return factory.updateFunctionDeclaration(d, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body); - case SyntaxKind.ClassDeclaration: - const decorators = canHaveDecorators(d) ? getDecorators(d) : undefined; - return factory.updateClassDeclaration(d, concatenate(decorators, modifiers), d.name, d.typeParameters, d.heritageClauses, d.members); - case SyntaxKind.VariableStatement: - return factory.updateVariableStatement(d, modifiers, d.declarationList); - case SyntaxKind.ModuleDeclaration: - return factory.updateModuleDeclaration(d, modifiers, d.name, d.body); - case SyntaxKind.EnumDeclaration: - return factory.updateEnumDeclaration(d, modifiers, d.name, d.members); - case SyntaxKind.TypeAliasDeclaration: - return factory.updateTypeAliasDeclaration(d, modifiers, d.name, d.typeParameters, d.type); - case SyntaxKind.InterfaceDeclaration: - return factory.updateInterfaceDeclaration(d, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); - case SyntaxKind.ImportEqualsDeclaration: - return factory.updateImportEqualsDeclaration(d, modifiers, d.isTypeOnly, d.name, d.moduleReference); - case SyntaxKind.ExpressionStatement: - return Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...` - default: - return Debug.assertNever(d, `Unexpected declaration kind ${(d as DeclarationStatement).kind}`); - } -} -function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly Statement[] | undefined { - return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)]; -} -function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] { - switch (decl.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - return [decl.name!.text]; // TODO: GH#18217 - case SyntaxKind.VariableStatement: - return mapDefined(decl.declarationList.declarations, d => isIdentifier(d.name) ? d.name.text : undefined); - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - return emptyArray; - case SyntaxKind.ExpressionStatement: - return Debug.fail("Can't export an ExpressionStatement"); // Shouldn't try to add 'export' keyword to `exports.x = ...` - default: - return Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`); - } -} - -/** Creates `exports.x = x;` */ -function createExportAssignment(name: string): Statement { - return factory.createExpressionStatement( - factory.createBinaryExpression( - factory.createPropertyAccessExpression(factory.createIdentifier("exports"), factory.createIdentifier(name)), - SyntaxKind.EqualsToken, - factory.createIdentifier(name))); -} diff --git a/src/services/services.ts b/src/services/services.ts index acd845c7979c6..80e88ced67e49 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -65,6 +65,8 @@ import { EntityName, equateValues, ExportDeclaration, + Extension, + extensionFromPath, FileReference, FileTextChanges, filter, @@ -86,6 +88,7 @@ import { getAdjustedRenameLocation, getAllSuperTypeNodes, getAssignmentDeclarationKind, + getBaseFileName, GetCompletionsAtPositionOptions, getContainerNode, getDefaultLibFileName, @@ -134,6 +137,7 @@ import { InlayHints, InlayHintsContext, insertSorted, + InteractiveRefactorArguments, InterfaceType, IntersectionType, isArray, @@ -276,6 +280,7 @@ import { SourceFile, SourceFileLike, SourceMapSource, + startsWith, Statement, stringContains, StringLiteral, @@ -319,6 +324,7 @@ import { } from "./_namespaces/ts"; import * as NavigateTo from "./_namespaces/ts.NavigateTo"; import * as NavigationBar from "./_namespaces/ts.NavigationBar"; +import { createNewFileName } from "./_namespaces/ts.refactor"; import * as classifier from "./classifier"; import * as classifier2020 from "./classifier2020"; @@ -2977,6 +2983,19 @@ export function createLanguageService( return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind), includeInteractiveActions); } + function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): { newFileName: string, files: string[] } { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const allFiles = Debug.checkDefined(program.getSourceFiles()); + const extension = extensionFromPath(fileName); + const files = mapDefined(allFiles, file => !program?.isSourceFileFromExternalLibrary(sourceFile) && + !(sourceFile === getValidSourceFile(file.fileName) || extension === Extension.Ts && extensionFromPath(file.fileName) === Extension.Dts || extension === Extension.Dts && startsWith(getBaseFileName(file.fileName), "lib.") && extensionFromPath(file.fileName) === Extension.Dts) + && extension === extensionFromPath(file.fileName) ? file.fileName : undefined); + + const newFileName = createNewFileName(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host); + return { newFileName, files }; + } + function getEditsForRefactor( fileName: string, formatOptions: FormatCodeSettings, @@ -2984,10 +3003,11 @@ export function createLanguageService( refactorName: string, actionName: string, preferences: UserPreferences = emptyOptions, + interactiveRefactorArguments?: InteractiveRefactorArguments, ): RefactorEditInfo | undefined { synchronizeHostData(); const file = getValidSourceFile(fileName); - return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName); + return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName, interactiveRefactorArguments); } function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { @@ -3084,6 +3104,7 @@ export function createLanguageService( updateIsDefinitionOfReferencedSymbols, getApplicableRefactors, getEditsForRefactor, + getMoveToRefactoringFileSuggestions, toLineColumnOffset, getSourceMapper: () => sourceMapper, clearSourceMapperCache: () => sourceMapper.clearCache(), diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 7ad7bf4cc3368..aac18cce972ed 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -10,6 +10,7 @@ import { concatenate, ConstructorDeclaration, contains, + createMultiMap, createNodeFactory, createPrinter, createRange, @@ -116,6 +117,7 @@ import { mapDefined, MethodSignature, Modifier, + MultiMap, NamedImportBindings, NamedImports, NamespaceImport, @@ -337,6 +339,11 @@ interface ChangeText extends BaseChange { readonly text: string; } +interface NewFileInsertion { + readonly oldFile?: SourceFile; + readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]; +} + function getAdjustedRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd): TextRange { return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) }; } @@ -480,7 +487,7 @@ export function isThisTypeAnnotatable(containingFunction: SignatureDeclaration): /** @internal */ export class ChangeTracker { private readonly changes: Change[] = []; - private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; + private newFileChanges?: MultiMap ; private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; @@ -622,6 +629,13 @@ export class ChangeTracker { } } + private insertStatementsInNewFile(fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], oldFile?: SourceFile): void { + if (!this.newFileChanges) { + this.newFileChanges = createMultiMap(); + } + this.newFileChanges.add(fileName, { oldFile, statements }); + } + public insertFirstParameter(sourceFile: SourceFile, parameters: NodeArray, newParam: ParameterDeclaration): void { const p0 = firstOrUndefined(parameters); if (p0) { @@ -1128,14 +1142,16 @@ export class ChangeTracker { this.finishDeleteDeclarations(); this.finishClassesWithNodesInsertedAtStart(); const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); - for (const { oldFile, fileName, statements } of this.newFiles) { - changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); + if (this.newFileChanges) { + this.newFileChanges.forEach((insertions, fileName) => { + changes.push(changesToText.newFileChanges(fileName, insertions, this.newLineCharacter, this.formatContext)); + }); } return changes; } public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { - this.newFiles.push({ oldFile, fileName, statements }); + this.insertStatementsInNewFile(fileName, statements, oldFile); } } @@ -1207,11 +1223,6 @@ function getMembersOrProperties(node: ClassLikeDeclaration | InterfaceDeclaratio /** @internal */ export type ValidateNonFormattedText = (node: Node, text: string) => void; -/** @internal */ -export function getNewFileText(statements: readonly Statement[], scriptKind: ScriptKind, newLineCharacter: string, formatContext: formatting.FormatContext): string { - return changesToText.newFileChangesWorker(/*oldFile*/ undefined, scriptKind, statements, newLineCharacter, formatContext); -} - namespace changesToText { export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] { return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => { @@ -1228,7 +1239,6 @@ namespace changesToText { const textChanges = mapDefined(normalized, c => { const span = createTextSpanFromRange(c.range); const newText = computeNewText(c, sourceFile, newLineCharacter, formatContext, validate); - // Filter out redundant changes. if (span.length === newText.length && stringContainsAt(sourceFile.text, newText, span.start)) { return undefined; @@ -1241,14 +1251,14 @@ namespace changesToText { }); } - export function newFileChanges(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges { - const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext); + export function newFileChanges(fileName: string, insertions: readonly NewFileInsertion[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges { + const text = newFileChangesWorker(getScriptKindFromFileName(fileName), insertions, newLineCharacter, formatContext); return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; } - export function newFileChangesWorker(oldFile: SourceFile | undefined, scriptKind: ScriptKind, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): string { + export function newFileChangesWorker(scriptKind: ScriptKind, insertions: readonly NewFileInsertion[], newLineCharacter: string, formatContext: formatting.FormatContext): string { // TODO: this emits the file, parses it back, then formats it that -- may be a less roundabout way to do this - const nonFormattedText = statements.map(s => s === SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter); + const nonFormattedText = flatMap(insertions, insertion => insertion.statements.map(s => s === SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, insertion.oldFile, newLineCharacter).text)).join(newLineCharacter); const sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind); const changes = formatting.formatDocument(sourceFile, formatContext); return applyChanges(nonFormattedText, changes) + newLineCharacter; diff --git a/src/services/types.ts b/src/services/types.ts index 80d9400535b58..6898bd20cadba 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -637,7 +637,8 @@ export interface LanguageService { * arguments for any interactive action before offering it. */ getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractiveActions?: boolean): ApplicableRefactorInfo[]; - getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined, includeInteractiveActions?: InteractiveRefactorArguments): RefactorEditInfo | undefined; + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { newFileName: string, files: string[] }; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -1278,6 +1279,10 @@ export interface DocCommentTemplateOptions { readonly generateReturnInDocTemplate?: boolean; } +export interface InteractiveRefactorArguments { + targetFile: string; +} + export interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; @@ -1770,10 +1775,10 @@ export interface Refactor { kinds?: string[]; /** Compute the associated code actions */ - getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined; + getEditsForAction(context: RefactorContext, actionName: string, interactiveRefactorArguments?: InteractiveRefactorArguments): RefactorEditInfo | undefined; /** Compute (quickly) which actions are available here */ - getAvailableActions(context: RefactorContext, includeInteractive?: boolean): readonly ApplicableRefactorInfo[]; + getAvailableActions(context: RefactorContext, includeInteractive?: boolean, interactiveRefactorArguments?: InteractiveRefactorArguments): readonly ApplicableRefactorInfo[]; } /** @internal */ diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 92ece99ab74c7..d7c8149609b0c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -54,6 +54,7 @@ import { ElementAccessExpression, EmitFlags, EmitHint, + emitModuleKindIsNonNodeESM, emptyArray, EndOfFileToken, endsWith, @@ -88,8 +89,10 @@ import { getAssignmentDeclarationKind, getCombinedNodeFlagsAlwaysIncludeJSDoc, getDirectoryPath, + getEmitModuleKind, getEmitScriptTarget, getExternalModuleImportEqualsDeclarationExpression, + getImpliedNodeFormatForFile, getIndentString, getJSDocEnumTag, getLastChild, @@ -108,8 +111,10 @@ import { getTextOfIdentifierOrLiteral, getTextOfNode, getTypesPackageName, + hasJSFileExtension, hasSyntacticModifier, HeritageClause, + hostGetCanonicalFileName, Identifier, identifierIsThisKeyword, identity, @@ -267,6 +272,7 @@ import { ModifierFlags, ModuleDeclaration, ModuleInstanceState, + ModuleKind, ModuleResolutionKind, ModuleSpecifierResolutionHost, moduleSpecifiers, @@ -350,6 +356,7 @@ import { textSpanEnd, Token, tokenToString, + toPath, tryCast, Type, TypeChecker, @@ -3137,7 +3144,12 @@ export function getSynthesizedDeepClones(nodes: NodeArray, in export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia?: boolean): NodeArray | undefined; /** @internal */ export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia = true): NodeArray | undefined { - return nodes && factory.createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma); + if (nodes) { + const cloned = factory.createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma); + setTextRange(cloned, nodes); + return cloned; + } + return nodes; } /** @internal */ @@ -4165,3 +4177,45 @@ export function newCaseClauseTracker(checker: TypeChecker, clauses: readonly (Ca } } } + +/** @internal */ +export function fileShouldUseJavaScriptRequire(file: SourceFile | string, program: Program, host: LanguageServiceHost, preferRequire?: boolean) { + const fileName = typeof file === "string" ? file : file.fileName; + if (!hasJSFileExtension(fileName)) { + return false; + } + const compilerOptions = program.getCompilerOptions(); + const moduleKind = getEmitModuleKind(compilerOptions); + const impliedNodeFormat = typeof file === "string" + ? getImpliedNodeFormatForFile(toPath(file, host.getCurrentDirectory(), hostGetCanonicalFileName(host)), program.getPackageJsonInfoCache?.(), host, compilerOptions) + : file.impliedNodeFormat; + + if (impliedNodeFormat === ModuleKind.ESNext) { + return false; + } + if (impliedNodeFormat === ModuleKind.CommonJS) { + // Since we're in a JS file, assume the user is writing the JS that will run + // (i.e., assume `noEmit`), so a CJS-format file should just have require + // syntax, rather than imports that will be downleveled to `require`. + return true; + } + if (compilerOptions.verbatimModuleSyntax && moduleKind === ModuleKind.CommonJS) { + // Using ESM syntax under these options would result in an error. + return true; + } + if (compilerOptions.verbatimModuleSyntax && emitModuleKindIsNonNodeESM(moduleKind)) { + return false; + } + + // impliedNodeFormat is undefined and `verbatimModuleSyntax` is off (or in an invalid combo) + // Use heuristics from existing code + if (typeof file === "object") { + if (file.commonJsModuleIndicator) { + return true; + } + if (file.externalModuleIndicator) { + return false; + } + } + return preferRequire; +} \ No newline at end of file diff --git a/src/testRunner/tests.ts b/src/testRunner/tests.ts index 45a31fc7bbade..366d62ec4dc73 100644 --- a/src/testRunner/tests.ts +++ b/src/testRunner/tests.ts @@ -194,3 +194,4 @@ import "./unittests/tsserver/versionCache"; import "./unittests/tsserver/watchEnvironment"; import "./unittests/debugDeprecation"; import "./unittests/tsserver/inconsistentErrorInEditor"; +import "./unittests/tsserver/getMoveToRefactoringFileSuggestions"; diff --git a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts new file mode 100644 index 0000000000000..e70ab769cb052 --- /dev/null +++ b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts @@ -0,0 +1,117 @@ +import * as ts from "../../_namespaces/ts"; +import { + baselineTsserverLogs, + createLoggerWithInMemoryLogs, + createSession, + openFilesForSession +} from "../helpers/tsserver"; +import { + createServerHost, + File +} from "../helpers/virtualFileSystemWithWatch"; + +describe("unittests:: tsserver:: getMoveToRefactoringFileSuggestions", () => { + it("works for suggesting a list of files, excluding node_modules within a project", () => { + const file1: File = { + path: "/project/a/file1.ts", + content: `interface ka { + name: string; + } + ` + }; + const file2: File = { path: "/project/b/file2.ts", content: "" }; + const file3: File = { path: "/project/d/e/file3.ts", content: "" }; + const file4: File = { + path: "/project/a/file4.ts", + content: `import { value } from "../node_modules/@types/node/someFile.d.ts"; +import { value1 } from "../node_modules/.cache/someFile.d.ts";` + }; + const nodeModulesFile1: File = { + path: "project/node_modules/@types/node/someFile.d.ts", + content: `export const value = 0;` + }; + const nodeModulesFile2: File = { + path: "project/node_modules/.cache/someFile.d.ts", + content: `export const value1 = 0;` + }; + const tsconfig: File = { + path: "/project/tsconfig.json", + content: "{}", + }; + const host = createServerHost([file1, file2, file3, file3, file4, nodeModulesFile1, nodeModulesFile2, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file1], session); + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 11 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, excluding node_modules within a project", session); + }); + it("suggests only .ts file for a .ts filepath", () => { + const file1: File = { + path: "/file1.ts", + content: `interface ka { + name: string; + } + ` + }; + const file2: File = { path: "/file2.tsx", content: "" }; + const file3: File = { path: "/file3.mts", content: "" }; + const file4: File = { path: "/file4.cts", content: "" }; + const file5: File = { path: "/file5.js", content: "" }; + const file6: File = { path: "/file6.d.ts", content: "" }; + const file7: File = { path: "/file7.ts", content: "" }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file2.tsx", "./file3.mts", "./file4.cts", "./file5.js", "./file6.d.ts", "./file7.ts"] }) }; + + const host = createServerHost([file1, file2, file3, file4, file5, file6, file7, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file1], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 11 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "suggests only .ts file for a .ts filepath", session); + }); + it("suggests only .js file for a .js filepath", () => { + const file1: File = { + path: "/file1.js", + content: `class C {}` + }; + const file2: File = { path: "/file2.js", content: "" }; + const file3: File = { path: "/file3.mts", content: "" }; + const file4: File = { path: "/file4.ts", content: "" }; + const file5: File = { path: "/file5.js", content: "" }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.js", "./file2.js", "./file3.mts", "./file4.ts", "./file5.js"] }) }; + + const host = createServerHost([file1, file2, file3, file4, file5, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file1], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 7 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "suggests only .js file for a .js filepath", session); + }); + it("skips lib.d.ts files", () => { + const file1: File = { + path: "/file1.d.ts", + content: `class C {}` + }; + const file2: File = { path: "/a/lib.d.ts", content: "" }; + const file3: File = { path: "/a/file3.d.ts", content: "" }; + const file4: File = { path: "/a/lib.es6.d.ts", content: "" }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.d.ts", "./a/lib.d.ts", "./a/file3.d.ts", "/a/lib.es6.d.ts"] }) }; + + const host = createServerHost([file1, file2, file3, file4, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file1], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 7 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "skips lib.d.ts files", session); + }); +}); \ No newline at end of file diff --git a/src/testRunner/unittests/tsserver/refactors.ts b/src/testRunner/unittests/tsserver/refactors.ts index 6b38ba34878d0..49ae2f77aeb57 100644 --- a/src/testRunner/unittests/tsserver/refactors.ts +++ b/src/testRunner/unittests/tsserver/refactors.ts @@ -94,4 +94,30 @@ describe("unittests:: tsserver:: refactors", () => { }); baselineTsserverLogs("refactors", "handles canonicalization of tsconfig path", session); }); + + it("handles moving statement to an existing file", () => { + const aTs: File = { path: "/Foo/a.ts", content: "const x = 0;" }; + const bTs: File = { + path: "/Foo/b.ts", content: `import {} from "./bar"; +const a = 1;`}; + const tsconfig: File = { path: "/Foo/tsconfig.json", content: `{ "files": ["./a.ts", "./b.ts"] }` }; + const host = createServerHost([aTs, bTs, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([aTs], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetEditsForRefactor, + arguments: { + file: aTs.path, + startLine: 1, + startOffset: 1, + endLine: 2, + endOffset: aTs.content.length, + refactor: "Move to file", + action: "Move to file", + interactiveRefactorArguments: { targetFile: "/Foo/b.ts" }, + } + }); + baselineTsserverLogs("refactors", "handles moving statement to an existing file", session); + }); }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 4f0a7d8abdd46..5b45c6d0b58f0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -155,6 +155,7 @@ declare namespace ts { GetSupportedCodeFixes = "getSupportedCodeFixes", GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", + GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions", OrganizeImports = "organizeImports", GetEditsForFileRename = "getEditsForFileRename", ConfigurePlugin = "configurePlugin", @@ -518,6 +519,26 @@ declare namespace ts { interface GetApplicableRefactorsResponse extends Response { body?: ApplicableRefactorInfo[]; } + /** + * Request refactorings at a given position or selection area to move to an existing file. + */ + interface GetMoveToRefactoringFileSuggestionsRequest extends Request { + command: CommandTypes.GetMoveToRefactoringFileSuggestions; + arguments: GetMoveToRefactoringFileSuggestionsRequestArgs; + } + type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRangeRequestArgs & { + kind?: string; + }; + /** + * Response is a list of available files. + * Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring + */ + interface GetMoveToRefactoringFileSuggestions extends Response { + body: { + newFileName: string; + files: string[]; + }; + } /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ @@ -582,6 +603,7 @@ declare namespace ts { type GetEditsForRefactorRequestArgs = FileLocationOrRangeRequestArgs & { refactor: string; action: string; + interactiveRefactorArguments?: InteractiveRefactorArguments; }; interface GetEditsForRefactorResponse extends Response { body?: RefactorEditInfo; @@ -3989,6 +4011,7 @@ declare namespace ts { private getRange; private getApplicableRefactors; private getEditsForRefactor; + private getMoveToRefactoringFileSuggestions; private organizeImports; private getEditsForFileRename; private getCodeFixes; @@ -10149,7 +10172,11 @@ declare namespace ts { * arguments for any interactive action before offering it. */ getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractiveActions?: boolean): ApplicableRefactorInfo[]; - getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined, includeInteractiveActions?: InteractiveRefactorArguments): RefactorEditInfo | undefined; + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { + newFileName: string; + files: string[]; + }; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; @@ -10657,6 +10684,9 @@ declare namespace ts { interface DocCommentTemplateOptions { readonly generateReturnInDocTemplate?: boolean; } + interface InteractiveRefactorArguments { + targetFile: string; + } interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 5b2e1a9668e34..a7d30d6c3e1f5 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6205,7 +6205,11 @@ declare namespace ts { * arguments for any interactive action before offering it. */ getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractiveActions?: boolean): ApplicableRefactorInfo[]; - getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined, includeInteractiveActions?: InteractiveRefactorArguments): RefactorEditInfo | undefined; + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { + newFileName: string; + files: string[]; + }; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; @@ -6713,6 +6717,9 @@ declare namespace ts { interface DocCommentTemplateOptions { readonly generateReturnInDocTemplate?: boolean; } + interface InteractiveRefactorArguments { + targetFile: string; + } interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js new file mode 100644 index 0000000000000..5f107bc89a04e --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js @@ -0,0 +1,118 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.d.ts] +class C {} + +//// [/a/lib.d.ts] + + +//// [/a/file3.d.ts] + + +//// [/a/lib.es6.d.ts] + + +//// [/tsconfig.json] +{"files":["./file1.d.ts","./a/lib.d.ts","./a/file3.d.ts","/a/lib.es6.d.ts"]} + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/file1.d.ts" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: / +Info seq [hh:mm:ss:mss] For info: /file1.d.ts :: Config file name: /tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /tsconfig.json : { + "rootNames": [ + "/file1.d.ts", + "/a/lib.d.ts", + "/a/file3.d.ts", + "/a/lib.es6.d.ts" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/file3.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib.es6.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (4) + /file1.d.ts SVC-1-0 "class C {}" + /a/lib.d.ts Text-1 "" + /a/file3.d.ts Text-1 "" + /a/lib.es6.d.ts Text-1 "" + + + file1.d.ts + Part of 'files' list in tsconfig.json + a/lib.d.ts + Part of 'files' list in tsconfig.json + a/file3.d.ts + Part of 'files' list in tsconfig.json + a/lib.es6.d.ts + Part of 'files' list in tsconfig.json + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (4) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /file1.d.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/a/lib.d.ts: *new* + {} +/a/file3.d.ts: *new* + {} +/a/lib.es6.d.ts: *new* + {} + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.d.ts", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "response": { + "newFileName": "/C.d.ts", + "files": [ + "/a/file3.d.ts" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js new file mode 100644 index 0000000000000..0f9fe4cd95541 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js @@ -0,0 +1,129 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.js] +class C {} + +//// [/file2.js] + + +//// [/file3.mts] + + +//// [/file4.ts] + + +//// [/file5.js] + + +//// [/tsconfig.json] +{"files":["./file1.js","./file2.js","./file3.mts","./file4.ts","./file5.js"]} + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/file1.js" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: / +Info seq [hh:mm:ss:mss] For info: /file1.js :: Config file name: /tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /tsconfig.json : { + "rootNames": [ + "/file1.js", + "/file2.js", + "/file3.mts", + "/file4.ts", + "/file5.js" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file2.js 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file4.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (5) + /file1.js SVC-1-0 "class C {}" + /file2.js Text-1 "" + /file3.mts Text-1 "" + /file4.ts Text-1 "" + /file5.js Text-1 "" + + + file1.js + Part of 'files' list in tsconfig.json + file2.js + Part of 'files' list in tsconfig.json + file3.mts + Part of 'files' list in tsconfig.json + file4.ts + Part of 'files' list in tsconfig.json + file5.js + Part of 'files' list in tsconfig.json + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (5) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /file1.js ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/file2.js: *new* + {} +/file3.mts: *new* + {} +/file4.ts: *new* + {} +/file5.js: *new* + {} + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.js", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "response": { + "newFileName": "/C.js", + "files": [ + "/file2.js", + "/file5.js" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js new file mode 100644 index 0000000000000..656ad93ccbd26 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js @@ -0,0 +1,151 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.ts] +interface ka { + name: string; + } + + +//// [/file2.tsx] + + +//// [/file3.mts] + + +//// [/file4.cts] + + +//// [/file5.js] + + +//// [/file6.d.ts] + + +//// [/file7.ts] + + +//// [/tsconfig.json] +{"files":["./file1.ts","./file2.tsx","./file3.mts","./file4.cts","./file5.js","./file6.d.ts","./file7.ts"]} + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/file1.ts" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: / +Info seq [hh:mm:ss:mss] For info: /file1.ts :: Config file name: /tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /tsconfig.json : { + "rootNames": [ + "/file1.ts", + "/file2.tsx", + "/file3.mts", + "/file4.cts", + "/file5.js", + "/file6.d.ts", + "/file7.ts" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file2.tsx 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file4.cts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file6.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file7.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (7) + /file1.ts SVC-1-0 "interface ka {\n name: string;\n }\n " + /file2.tsx Text-1 "" + /file3.mts Text-1 "" + /file4.cts Text-1 "" + /file5.js Text-1 "" + /file6.d.ts Text-1 "" + /file7.ts Text-1 "" + + + file1.ts + Part of 'files' list in tsconfig.json + file2.tsx + Part of 'files' list in tsconfig.json + file3.mts + Part of 'files' list in tsconfig.json + file4.cts + Part of 'files' list in tsconfig.json + file5.js + Part of 'files' list in tsconfig.json + file6.d.ts + Part of 'files' list in tsconfig.json + file7.ts + Part of 'files' list in tsconfig.json + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (7) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /file1.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/file2.tsx: *new* + {} +/file3.mts: *new* + {} +/file4.cts: *new* + {} +/file5.js: *new* + {} +/file6.d.ts: *new* + {} +/file7.ts: *new* + {} + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.ts", + "line": 1, + "offset": 11 + }, + "seq": 2, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "response": { + "newFileName": "/ka.ts", + "files": [ + "/file7.ts" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js new file mode 100644 index 0000000000000..4b7ac756e01ad --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js @@ -0,0 +1,146 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/project/a/file1.ts] +interface ka { + name: string; + } + + +//// [/project/b/file2.ts] + + +//// [/project/d/e/file3.ts] + + +//// [/project/a/file4.ts] +import { value } from "../node_modules/@types/node/someFile.d.ts"; +import { value1 } from "../node_modules/.cache/someFile.d.ts"; + +//// [/project/node_modules/@types/node/someFile.d.ts] +export const value = 0; + +//// [/project/node_modules/.cache/someFile.d.ts] +export const value1 = 0; + +//// [/project/tsconfig.json] +{} + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/project/a/file1.ts" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: /project/a +Info seq [hh:mm:ss:mss] For info: /project/a/file1.ts :: Config file name: /project/tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /project/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/tsconfig.json 2000 undefined Project: /project/tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /project/tsconfig.json : { + "rootNames": [ + "/project/a/file1.ts", + "/project/a/file4.ts", + "/project/b/file2.ts", + "/project/d/e/file3.ts" + ], + "options": { + "configFilePath": "/project/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project 1 undefined Config: /project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project 1 undefined Config: /project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/a/file4.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/b/file2.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/d/e/file3.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /project/tsconfig.json +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /project/tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /project/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (6) + /project/a/file1.ts SVC-1-0 "interface ka {\n name: string;\n }\n " + /project/node_modules/@types/node/someFile.d.ts Text-1 "export const value = 0;" + /project/node_modules/.cache/someFile.d.ts Text-1 "export const value1 = 0;" + /project/a/file4.ts Text-1 "import { value } from \"../node_modules/@types/node/someFile.d.ts\";\nimport { value1 } from \"../node_modules/.cache/someFile.d.ts\";" + /project/b/file2.ts Text-1 "" + /project/d/e/file3.ts Text-1 "" + + + a/file1.ts + Matched by default include pattern '**/*' + node_modules/@types/node/someFile.d.ts + Imported via "../node_modules/@types/node/someFile.d.ts" from file 'a/file4.ts' + node_modules/.cache/someFile.d.ts + Imported via "../node_modules/.cache/someFile.d.ts" from file 'a/file4.ts' + a/file4.ts + Matched by default include pattern '**/*' + b/file2.ts + Matched by default include pattern '**/*' + d/e/file3.ts + Matched by default include pattern '**/*' + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (6) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /project/a/file1.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /project/tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/project/tsconfig.json: *new* + {} +/project/a/file4.ts: *new* + {} +/project/b/file2.ts: *new* + {} +/project/d/e/file3.ts: *new* + {} + +FsWatchesRecursive:: +/project: *new* + {} +/project/node_modules: *new* + {} + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/project/a/file1.ts", + "line": 1, + "offset": 11 + }, + "seq": 2, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "response": { + "newFileName": "/project/a/ka.ts", + "files": [ + "/project/a/file4.ts", + "/project/b/file2.ts", + "/project/d/e/file3.ts" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js new file mode 100644 index 0000000000000..019f6c912ce43 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js @@ -0,0 +1,112 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:19.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/blah/file1.ts] +class CC { } + +//// [/blah/file2.ts] + + +//// [/blah-tests/file3.ts] +import { value1 } from "../blah/file1.ts"; + +//// [/blah-tests/file4.ts] + + +//// [/blah/tsconfig.json] +{ "files": ["./file1.ts", "./file2.ts"] } + +//// [/blah-tests/tsconfig.json] +{ "files": ["./file3.ts", "./file4.ts"] } + + +Info 1 [00:00:20.000] request: + { + "command": "open", + "arguments": { + "file": "/blah/file1.ts" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:21.000] Search path: /blah +Info 3 [00:00:22.000] For info: /blah/file1.ts :: Config file name: /blah/tsconfig.json +Info 4 [00:00:23.000] Creating configuration project /blah/tsconfig.json +Info 5 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /blah/tsconfig.json 2000 undefined Project: /blah/tsconfig.json WatchType: Config file +Info 6 [00:00:25.000] Config: /blah/tsconfig.json : { + "rootNames": [ + "/blah/file1.ts", + "/blah/file2.ts" + ], + "options": { + "configFilePath": "/blah/tsconfig.json" + } +} +Info 7 [00:00:26.000] FileWatcher:: Added:: WatchInfo: /blah/file2.ts 500 undefined WatchType: Closed Script info +Info 8 [00:00:27.000] Starting updateGraphWorker: Project: /blah/tsconfig.json +Info 9 [00:00:28.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /blah/tsconfig.json WatchType: Missing file +Info 10 [00:00:29.000] DirectoryWatcher:: Added:: WatchInfo: /blah/node_modules/@types 1 undefined Project: /blah/tsconfig.json WatchType: Type roots +Info 11 [00:00:30.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /blah/node_modules/@types 1 undefined Project: /blah/tsconfig.json WatchType: Type roots +Info 12 [00:00:31.000] Finishing updateGraphWorker: Project: /blah/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 13 [00:00:32.000] Project '/blah/tsconfig.json' (Configured) +Info 14 [00:00:33.000] Files (2) + /blah/file1.ts SVC-1-0 "class CC { }" + /blah/file2.ts Text-1 "" + + + file1.ts + Part of 'files' list in tsconfig.json + file2.ts + Part of 'files' list in tsconfig.json + +Info 15 [00:00:34.000] ----------------------------------------------- +Info 16 [00:00:35.000] Project '/blah/tsconfig.json' (Configured) +Info 16 [00:00:36.000] Files (2) + +Info 16 [00:00:37.000] ----------------------------------------------- +Info 16 [00:00:38.000] Open files: +Info 16 [00:00:39.000] FileName: /blah/file1.ts ProjectRootPath: undefined +Info 16 [00:00:40.000] Projects: /blah/tsconfig.json +Info 16 [00:00:41.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} +/blah/node_modules/@types: *new* + {"pollingInterval":500} + +FsWatches:: +/blah/tsconfig.json: *new* + {} +/blah/file2.ts: *new* + {} + +Before request + +Info 17 [00:00:42.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/blah/file1.ts", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info 18 [00:00:43.000] response: + { + "response": { + "newFilename": "/blah/CC.ts", + "files": [ + "/blah/file1.ts", + "/blah/file2.ts" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js new file mode 100644 index 0000000000000..17da752e876eb --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js @@ -0,0 +1,201 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:09.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.js] +import {} from "./file.js"; + +//// [/file2.js] +class C {} + +//// [/jsconfig.json] +{"files":["./file1.js","./file.js"]} + + +Info 1 [00:00:10.000] request: + { + "command": "open", + "arguments": { + "file": "/file2.js" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:11.000] Search path: / +Info 3 [00:00:12.000] For info: /file2.js :: Config file name: /jsconfig.json +Info 4 [00:00:13.000] Creating configuration project /jsconfig.json +Info 5 [00:00:14.000] FileWatcher:: Added:: WatchInfo: /jsconfig.json 2000 undefined Project: /jsconfig.json WatchType: Config file +Info 6 [00:00:15.000] Config: /jsconfig.json : { + "rootNames": [ + "/file1.js", + "/file.js" + ], + "options": { + "allowJs": true, + "maxNodeModuleJsDepth": 2, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "noEmit": true, + "configFilePath": "/jsconfig.json" + } +} +Info 7 [00:00:16.000] FileWatcher:: Added:: WatchInfo: /file1.js 500 undefined WatchType: Closed Script info +Info 8 [00:00:17.000] Starting updateGraphWorker: Project: /jsconfig.json +Info 9 [00:00:18.000] DirectoryWatcher:: Added:: WatchInfo: /file.js 1 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 10 [00:00:19.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /file.js 1 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 11 [00:00:20.000] DirectoryWatcher:: Added:: WatchInfo: 0 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 12 [00:00:21.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: 0 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 13 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /file.js 500 undefined Project: /jsconfig.json WatchType: Missing file +Info 14 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /jsconfig.json WatchType: Missing file +Info 15 [00:00:24.000] Finishing updateGraphWorker: Project: /jsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 16 [00:00:25.000] Project '/jsconfig.json' (Configured) +Info 17 [00:00:26.000] Files (1) + /file1.js Text-1 "import {} from \"./file.js\";" + + + file1.js + Part of 'files' list in tsconfig.json + +Info 18 [00:00:27.000] ----------------------------------------------- +TI:: Creating typing installer + +PolledWatches:: +/file.js: *new* + {"pollingInterval":500} +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/jsconfig.json: *new* + {} +/file1.js: *new* + {} +/: *new* + {} + +TI:: [00:00:28.000] Global cache location '/a/data/', safe file path '/safeList.json', types map path /typesMap.json +TI:: [00:00:29.000] Processing cache location '/a/data/' +TI:: [00:00:30.000] Trying to find '/a/data/package.json'... +TI:: [00:00:31.000] Finished processing cache location '/a/data/' +TI:: [00:00:32.000] Npm config file: /a/data/package.json +TI:: [00:00:33.000] Npm config file: '/a/data/package.json' is missing, creating new one... +Info 19 [00:00:36.000] DirectoryWatcher:: Triggered with a :: WatchInfo: 0 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 20 [00:00:37.000] Elapsed:: *ms DirectoryWatcher:: Triggered with a :: WatchInfo: 0 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +TI:: [00:00:42.000] Updating types-registry npm package... +TI:: [00:00:43.000] npm install --ignore-scripts types-registry@latest +TI:: [00:00:50.000] TI:: Updated types-registry npm package +TI:: typing installer creation complete +//// [/a/data/package.json] +{ "private": true } + +//// [/a/data/node_modules/types-registry/index.json] +{ + "entries": {} +} + + +TI:: [00:00:51.000] Got install request {"projectName":"/jsconfig.json","fileNames":["/file1.js"],"compilerOptions":{"allowJs":true,"maxNodeModuleJsDepth":2,"allowSyntheticDefaultImports":true,"skipLibCheck":true,"noEmit":true,"configFilePath":"/jsconfig.json","allowNonTsExtensions":true},"typeAcquisition":{"enable":true,"include":[],"exclude":[]},"unresolvedImports":[],"projectRootPath":"/","cachePath":"/a/data/","kind":"discover"} +TI:: [00:00:52.000] Request specifies cache path '/a/data/', loading cached information... +TI:: [00:00:53.000] Processing cache location '/a/data/' +TI:: [00:00:54.000] Cache location was already processed... +TI:: [00:00:55.000] Failed to load safelist from types map file '/typesMap.json' +TI:: [00:00:56.000] Explicitly included types: [] +TI:: [00:00:57.000] Inferred typings from unresolved imports: [] +TI:: [00:00:58.000] Result: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/bower_components","/node_modules"]} +TI:: [00:00:59.000] Finished typings discovery: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/bower_components","/node_modules"]} +TI:: [00:01:00.000] DirectoryWatcher:: Added:: WatchInfo: /bower_components +TI:: [00:01:01.000] DirectoryWatcher:: Added:: WatchInfo: /bower_components 1 undefined Project: /jsconfig.json watcher already invoked: false +TI:: [00:01:02.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /bower_components 1 undefined Project: /jsconfig.json watcher already invoked: false +TI:: [00:01:03.000] DirectoryWatcher:: Added:: WatchInfo: /node_modules +TI:: [00:01:04.000] DirectoryWatcher:: Added:: WatchInfo: /node_modules 1 undefined Project: /jsconfig.json watcher already invoked: false +TI:: [00:01:05.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /node_modules 1 undefined Project: /jsconfig.json watcher already invoked: false +TI:: [00:01:06.000] Sending response: + {"projectName":"/jsconfig.json","typeAcquisition":{"enable":true,"include":[],"exclude":[]},"compilerOptions":{"allowJs":true,"maxNodeModuleJsDepth":2,"allowSyntheticDefaultImports":true,"skipLibCheck":true,"noEmit":true,"configFilePath":"/jsconfig.json","allowNonTsExtensions":true},"typings":[],"unresolvedImports":[],"kind":"action::set"} +TI:: [00:01:07.000] No new typings were requested as a result of typings discovery +Info 21 [00:01:08.000] Starting updateGraphWorker: Project: /dev/null/inferredProject1* +Info 22 [00:01:09.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /dev/null/inferredProject1* WatchType: Missing file +Info 23 [00:01:10.000] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 24 [00:01:11.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 25 [00:01:12.000] Files (1) + /file2.js SVC-1-0 "class C {}" + + + file2.js + Root file specified for compilation + +Info 26 [00:01:13.000] ----------------------------------------------- +TI:: [00:01:14.000] Got install request {"projectName":"/dev/null/inferredProject1*","fileNames":["/file2.js"],"compilerOptions":{"target":1,"jsx":1,"allowNonTsExtensions":true,"allowJs":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typeAcquisition":{"enable":true,"include":[],"exclude":[]},"unresolvedImports":[],"projectRootPath":"/","cachePath":"/a/data/","kind":"discover"} +TI:: [00:01:15.000] Request specifies cache path '/a/data/', loading cached information... +TI:: [00:01:16.000] Processing cache location '/a/data/' +TI:: [00:01:17.000] Cache location was already processed... +TI:: [00:01:18.000] Explicitly included types: [] +TI:: [00:01:19.000] Inferred typings from unresolved imports: [] +TI:: [00:01:20.000] Result: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/bower_components","/node_modules"]} +TI:: [00:01:21.000] Finished typings discovery: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/bower_components","/node_modules"]} +TI:: [00:01:22.000] DirectoryWatcher:: Added:: WatchInfo: /bower_components +TI:: [00:01:23.000] DirectoryWatcher:: Added:: WatchInfo: /bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false +TI:: [00:01:24.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false +TI:: [00:01:25.000] DirectoryWatcher:: Added:: WatchInfo: /node_modules +TI:: [00:01:26.000] DirectoryWatcher:: Added:: WatchInfo: /node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false +TI:: [00:01:27.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false +TI:: [00:01:28.000] Sending response: + {"projectName":"/dev/null/inferredProject1*","typeAcquisition":{"enable":true,"include":[],"exclude":[]},"compilerOptions":{"target":1,"jsx":1,"allowNonTsExtensions":true,"allowJs":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typings":[],"unresolvedImports":[],"kind":"action::set"} +TI:: [00:01:29.000] No new typings were requested as a result of typings discovery +Info 27 [00:01:30.000] Project '/jsconfig.json' (Configured) +Info 27 [00:01:31.000] Files (1) + +Info 27 [00:01:32.000] ----------------------------------------------- +Info 27 [00:01:33.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 27 [00:01:34.000] Files (1) + +Info 27 [00:01:35.000] ----------------------------------------------- +Info 27 [00:01:36.000] Open files: +Info 27 [00:01:37.000] FileName: /file2.js ProjectRootPath: undefined +Info 27 [00:01:38.000] Projects: /dev/null/inferredProject1* +Info 27 [00:01:39.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/file.js: + {"pollingInterval":500} +/a/lib/lib.d.ts: + {"pollingInterval":500} +/bower_components: *new* + {"pollingInterval":500} +/node_modules: *new* + {"pollingInterval":500} + +FsWatches:: +/jsconfig.json: + {} +/file1.js: + {} +/: + {} + +Before request + +Info 28 [00:01:40.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file2.js", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info 29 [00:01:41.000] response: + { + "response": { + "newFilename": "/C.js", + "files": [ + "/file2.js" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js new file mode 100644 index 0000000000000..5294fb0b108bb --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js @@ -0,0 +1,123 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:09.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.ts] +import {} from "./file.ts"; + +//// [/file2.ts] +interface ka { + name: string; + } + + +//// [/tsconfig.json] +{"files":["./file1.ts","./file.ts"]} + + +Info 1 [00:00:10.000] request: + { + "command": "open", + "arguments": { + "file": "/file2.ts" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:11.000] Search path: / +Info 3 [00:00:12.000] For info: /file2.ts :: Config file name: /tsconfig.json +Info 4 [00:00:13.000] Creating configuration project /tsconfig.json +Info 5 [00:00:14.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:15.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.ts", + "/file.ts" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:16.000] FileWatcher:: Added:: WatchInfo: /file1.ts 500 undefined WatchType: Closed Script info +Info 8 [00:00:17.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 9 [00:00:18.000] DirectoryWatcher:: Added:: WatchInfo: /file.ts 1 undefined Project: /tsconfig.json WatchType: Failed Lookup Locations +Info 10 [00:00:19.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /file.ts 1 undefined Project: /tsconfig.json WatchType: Failed Lookup Locations +Info 11 [00:00:20.000] DirectoryWatcher:: Added:: WatchInfo: 0 undefined Project: /tsconfig.json WatchType: Failed Lookup Locations +Info 12 [00:00:21.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: 0 undefined Project: /tsconfig.json WatchType: Failed Lookup Locations +Info 13 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /file.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 14 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 15 [00:00:24.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 16 [00:00:25.000] Project '/tsconfig.json' (Configured) +Info 17 [00:00:26.000] Files (1) + /file1.ts Text-1 "import {} from \"./file.ts\";" + + + file1.ts + Part of 'files' list in tsconfig.json + +Info 18 [00:00:27.000] ----------------------------------------------- +Info 19 [00:00:28.000] Starting updateGraphWorker: Project: /dev/null/inferredProject1* +Info 20 [00:00:29.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /dev/null/inferredProject1* WatchType: Missing file +Info 21 [00:00:30.000] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 22 [00:00:31.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 23 [00:00:32.000] Files (1) + /file2.ts SVC-1-0 "interface ka {\n name: string;\n }\n " + + + file2.ts + Root file specified for compilation + +Info 24 [00:00:33.000] ----------------------------------------------- +Info 25 [00:00:34.000] Project '/tsconfig.json' (Configured) +Info 25 [00:00:35.000] Files (1) + +Info 25 [00:00:36.000] ----------------------------------------------- +Info 25 [00:00:37.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 25 [00:00:38.000] Files (1) + +Info 25 [00:00:39.000] ----------------------------------------------- +Info 25 [00:00:40.000] Open files: +Info 25 [00:00:41.000] FileName: /file2.ts ProjectRootPath: undefined +Info 25 [00:00:42.000] Projects: /dev/null/inferredProject1* +Info 25 [00:00:43.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/file.ts: *new* + {"pollingInterval":500} +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/file1.ts: *new* + {} +/: *new* + {} + +Before request + +Info 26 [00:00:44.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file2.ts", + "line": 1, + "offset": 11 + }, + "seq": 2, + "type": "request" + } +Info 27 [00:00:45.000] response: + { + "response": { + "newFilename": "/ka.ts", + "files": [ + "/file2.ts" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js new file mode 100644 index 0000000000000..3946f44852f49 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js @@ -0,0 +1,145 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:17.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.ts] +interface ka { + name: string; + } + + +//// [/file2.tsx] + + +//// [/file3.mts] + + +//// [/file4.cts] + + +//// [/file5.js] + + +//// [/file6.d.ts] + + +//// [/tsconfig.json] +{"files":["./file1.ts","./file2.tsx","./file3.mts","./file4.cts","./file5.js","./file6.d.ts"]} + + +Info 1 [00:00:18.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.ts" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:19.000] Search path: / +Info 3 [00:00:20.000] For info: /file1.ts :: Config file name: /tsconfig.json +Info 4 [00:00:21.000] Creating configuration project /tsconfig.json +Info 5 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:23.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.ts", + "/file2.tsx", + "/file3.mts", + "/file4.cts", + "/file5.js", + "/file6.d.ts" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /file2.tsx 500 undefined WatchType: Closed Script info +Info 8 [00:00:25.000] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info 9 [00:00:26.000] FileWatcher:: Added:: WatchInfo: /file4.cts 500 undefined WatchType: Closed Script info +Info 10 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info 11 [00:00:28.000] FileWatcher:: Added:: WatchInfo: /file6.d.ts 500 undefined WatchType: Closed Script info +Info 12 [00:00:29.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 13 [00:00:30.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 14 [00:00:31.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 15 [00:00:32.000] Project '/tsconfig.json' (Configured) +Info 16 [00:00:33.000] Files (6) + /file1.ts SVC-1-0 "interface ka {\n name: string;\n }\n " + /file2.tsx Text-1 "" + /file3.mts Text-1 "" + /file4.cts Text-1 "" + /file5.js Text-1 "" + /file6.d.ts Text-1 "" + + + file1.ts + Part of 'files' list in tsconfig.json + file2.tsx + Part of 'files' list in tsconfig.json + file3.mts + Part of 'files' list in tsconfig.json + file4.cts + Part of 'files' list in tsconfig.json + file5.js + Part of 'files' list in tsconfig.json + file6.d.ts + Part of 'files' list in tsconfig.json + +Info 17 [00:00:34.000] ----------------------------------------------- +Info 18 [00:00:35.000] Project '/tsconfig.json' (Configured) +Info 18 [00:00:36.000] Files (6) + +Info 18 [00:00:37.000] ----------------------------------------------- +Info 18 [00:00:38.000] Open files: +Info 18 [00:00:39.000] FileName: /file1.ts ProjectRootPath: undefined +Info 18 [00:00:40.000] Projects: /tsconfig.json +Info 18 [00:00:41.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/file2.tsx: *new* + {} +/file3.mts: *new* + {} +/file4.cts: *new* + {} +/file5.js: *new* + {} +/file6.d.ts: *new* + {} + +Before request + +Info 19 [00:00:42.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.ts", + "line": 1, + "offset": 11 + }, + "seq": 2, + "type": "request" + } +Info 20 [00:00:43.000] response: + { + "response": { + "newFilename": "/ka.ts", + "files": [ + "/file1.ts", + "/file2.tsx", + "/file3.mts", + "/file4.cts", + "/file6.d.ts" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js new file mode 100644 index 0000000000000..c39d3b43ed599 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js @@ -0,0 +1,130 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:15.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.js] +class C {} + +//// [/file2.js] + + +//// [/file3.mts] + + +//// [/file4.ts] + + +//// [/file5.js] + + +//// [/tsconfig.json] +{"files":["./file1.js","./file2.js","./file3.mts","./file4.ts","./file5.js"]} + + +Info 1 [00:00:16.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.js" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:17.000] Search path: / +Info 3 [00:00:18.000] For info: /file1.js :: Config file name: /tsconfig.json +Info 4 [00:00:19.000] Creating configuration project /tsconfig.json +Info 5 [00:00:20.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:21.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.js", + "/file2.js", + "/file3.mts", + "/file4.ts", + "/file5.js" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /file2.js 500 undefined WatchType: Closed Script info +Info 8 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info 9 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /file4.ts 500 undefined WatchType: Closed Script info +Info 10 [00:00:25.000] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info 11 [00:00:26.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 12 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 13 [00:00:28.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 14 [00:00:29.000] Project '/tsconfig.json' (Configured) +Info 15 [00:00:30.000] Files (5) + /file1.js SVC-1-0 "class C {}" + /file2.js Text-1 "" + /file3.mts Text-1 "" + /file4.ts Text-1 "" + /file5.js Text-1 "" + + + file1.js + Part of 'files' list in tsconfig.json + file2.js + Part of 'files' list in tsconfig.json + file3.mts + Part of 'files' list in tsconfig.json + file4.ts + Part of 'files' list in tsconfig.json + file5.js + Part of 'files' list in tsconfig.json + +Info 16 [00:00:31.000] ----------------------------------------------- +Info 17 [00:00:32.000] Project '/tsconfig.json' (Configured) +Info 17 [00:00:33.000] Files (5) + +Info 17 [00:00:34.000] ----------------------------------------------- +Info 17 [00:00:35.000] Open files: +Info 17 [00:00:36.000] FileName: /file1.js ProjectRootPath: undefined +Info 17 [00:00:37.000] Projects: /tsconfig.json +Info 17 [00:00:38.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/file2.js: *new* + {} +/file3.mts: *new* + {} +/file4.ts: *new* + {} +/file5.js: *new* + {} + +Before request + +Info 18 [00:00:39.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.js", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info 19 [00:00:40.000] response: + { + "response": { + "newFilename": "/C.js", + "files": [ + "/file1.js", + "/file2.js", + "/file5.js" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js new file mode 100644 index 0000000000000..8f6e4a458ba4a --- /dev/null +++ b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js @@ -0,0 +1,136 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/Foo/a.ts] +const x = 0; + +//// [/Foo/b.ts] +import {} from "./bar"; +const a = 1; + +//// [/Foo/tsconfig.json] +{ "files": ["./a.ts", "./b.ts"] } + + +Info seq [hh:mm:ss:mss] request: + { + "command": "open", + "arguments": { + "file": "/Foo/a.ts" + }, + "seq": 1, + "type": "request" + } +Info seq [hh:mm:ss:mss] Search path: /Foo +Info seq [hh:mm:ss:mss] For info: /Foo/a.ts :: Config file name: /Foo/tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /Foo/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /Foo/tsconfig.json 2000 undefined Project: /Foo/tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /Foo/tsconfig.json : { + "rootNames": [ + "/Foo/a.ts", + "/Foo/b.ts" + ], + "options": { + "configFilePath": "/Foo/tsconfig.json" + } +} +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /Foo/b.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /Foo/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /Foo/tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /Foo/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/Foo/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + /Foo/a.ts SVC-1-0 "const x = 0;" + /Foo/b.ts Text-1 "import {} from \"./bar\";\nconst a = 1;" + + + a.ts + Part of 'files' list in tsconfig.json + b.ts + Part of 'files' list in tsconfig.json + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/Foo/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) + +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /Foo/a.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /Foo/tsconfig.json +Info seq [hh:mm:ss:mss] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/foo/tsconfig.json: *new* + {} +/foo/b.ts: *new* + {} + +Before request + +Info seq [hh:mm:ss:mss] request: + { + "command": "getEditsForRefactor", + "arguments": { + "file": "/Foo/a.ts", + "startLine": 1, + "startOffset": 1, + "endLine": 2, + "endOffset": 12, + "refactor": "Move to file", + "action": "Move to file", + "interactiveRefactorArguments": { + "targetFile": "/Foo/b.ts" + } + }, + "seq": 2, + "type": "request" + } +Info seq [hh:mm:ss:mss] response: + { + "response": { + "edits": [ + { + "fileName": "/Foo/a.ts", + "textChanges": [ + { + "start": { + "line": 1, + "offset": 1 + }, + "end": { + "line": 1, + "offset": 13 + }, + "newText": "" + } + ] + }, + { + "fileName": "/Foo/b.ts", + "textChanges": [ + { + "start": { + "line": 2, + "offset": 13 + }, + "end": { + "line": 2, + "offset": 13 + }, + "newText": "\nconst x = 0;\n" + } + ] + } + ] + }, + "responseRequired": true + } +After request diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 2bf83ba52e5f7..9cf274e618978 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -198,6 +198,9 @@ declare namespace FourSlashInterface { readonly semicolons?: ts.SemicolonPreference; readonly indentSwitchCase?: boolean; } + interface InteractiveRefactorArguments { + targetFile: string; + } interface Range { fileName: string; pos: number; @@ -431,7 +434,11 @@ declare namespace FourSlashInterface { readonly preferences?: UserPreferences; }): void; noMoveToNewFile(): void; - + moveToFile(options: { + readonly newFileContents: { readonly [fileName: string]: string }; + readonly interactiveRefactorArguments: InteractiveRefactorArguments; + readonly preferences?: UserPreferences; + }): void; generateTypes(...options: GenerateTypesOptions[]): void; organizeImports(newContent: string, mode?: ts.OrganizeImportsMode, preferences?: UserPreferences): void; diff --git a/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts b/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts new file mode 100644 index 0000000000000..f49d27744bc07 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts @@ -0,0 +1,33 @@ +/// + +//@Filename: /bar.ts +////import './blah'; +//// +////const a = 2; + +// @Filename: /a.ts +////import { b } from './other'; +//// +////[|const y = b + 10;|] +////y; + +// @Filename: /other.ts +////export const b = 1; + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`import { y } from './bar'; + +y;`, + + "/bar.ts": +`import './blah'; +import { b } from './other'; + +const a = 2; +export const y = b + 10; +`, + }, + interactiveRefactorArguments: {targetFile: "/bar.ts"}, +}); diff --git a/tests/cases/fourslash/moveToFile_blankExistingFile.ts b/tests/cases/fourslash/moveToFile_blankExistingFile.ts new file mode 100644 index 0000000000000..dda33fca96f55 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_blankExistingFile.ts @@ -0,0 +1,28 @@ +/// + +// @Filename: /bar.ts +////// + +// @Filename: /a.ts +////import { b } from './other'; +////const p = 0; +////[|const y: Date = p + b;|] + +// @Filename: /other.ts +////export const b = 1; + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`export const p = 0; +`, + + "/bar.ts": +`import { b } from './other'; +import { p } from './a'; + +const y: Date = p + b; +`, + }, + interactiveRefactorArguments: {targetFile: "/bar.ts"}, +}); diff --git a/tests/cases/fourslash/moveToFile_ctsTomts.ts b/tests/cases/fourslash/moveToFile_ctsTomts.ts new file mode 100644 index 0000000000000..2a911fa05c7c0 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_ctsTomts.ts @@ -0,0 +1,36 @@ +/// + +// @module: nodenext +// @allowImportingTsExtensions: true +// @noEmit: true + +//@Filename: /src/dir2/bar.mts +////import './blah.ts'; +////const a = 2; + +// @Filename: /src/dir1/a.cts +////import { b } from './other.cts'; +////const p = 0; +////[|const y = p + b;|] +////y; + +// @Filename: /src/dir1/other.cts +////export const b = 1; + +verify.moveToFile({ + newFileContents: { + "/src/dir1/a.cts": +`import { y } from '../dir2/bar.mts'; +export const p = 0; +y;`, + + "/src/dir2/bar.mts": +`import { p } from '../dir1/a.cts'; +import { b } from '../dir1/other.cts'; +import './blah.ts'; +const a = 2; +export const y = p + b; +`, + }, + interactiveRefactorArguments: { targetFile: "/src/dir2/bar.mts" }, +}); diff --git a/tests/cases/fourslash/moveToFile_differentDirectories.ts b/tests/cases/fourslash/moveToFile_differentDirectories.ts new file mode 100644 index 0000000000000..2c9a1c544c116 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_differentDirectories.ts @@ -0,0 +1,32 @@ +/// + +//@moduleResolution: bundler +//@module: esnext + +// @Filename: /src/dir1/a.ts +////import { b } from './other'; +////[|const y = b + 10;|] +////y; + +// @Filename: /src/dir1/other.ts +////export const b = 1; + +//@Filename: /src/dir2/bar.ts +////import './blah'; +////const a = 2; + +verify.moveToFile({ + newFileContents: { + "/src/dir1/a.ts": +`import { y } from '../dir2/bar'; +y;`, + + "/src/dir2/bar.ts": +`import { b } from '../dir1/other'; +import './blah'; +const a = 2; +export const y = b + 10; +`, + }, + interactiveRefactorArguments: { targetFile: "/src/dir2/bar.ts" }, +}); diff --git a/tests/cases/fourslash/moveToFile_differentDirectories2.ts b/tests/cases/fourslash/moveToFile_differentDirectories2.ts new file mode 100644 index 0000000000000..4dba2881f189c --- /dev/null +++ b/tests/cases/fourslash/moveToFile_differentDirectories2.ts @@ -0,0 +1,33 @@ +/// + +//@moduleResolution: bundler +//@module: esnext + +// @Filename: /src/dir1/a.ts +////import { b } from './other'; +////const a = 10; +////[|const y = b + a;|] +////y; + +// @Filename: /src/dir1/other.ts +////export const b = 1; + +//@Filename: /src/dir2/bar.ts +//// + +verify.moveToFile({ + newFileContents: { + "/src/dir1/a.ts": +`import { y } from '../dir2/bar'; +export const a = 10; +y;`, + + "/src/dir2/bar.ts": +`import { b } from '../dir1/other'; +import { a } from '../dir1/a'; + +export const y = b + a; +`, + }, + interactiveRefactorArguments: { targetFile: "/src/dir2/bar.ts" } +}); diff --git a/tests/cases/fourslash/moveToFile_impossibleImport.ts b/tests/cases/fourslash/moveToFile_impossibleImport.ts new file mode 100644 index 0000000000000..08f8b5e38a5a3 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_impossibleImport.ts @@ -0,0 +1,37 @@ +/// + +// @module: nodenext + +// @Filename: /bar.cts +////const a = 2; + +// @Filename: /node_modules/esm-only/package.json +//// { +//// "name": "esm-only", +//// "version": "1.0.0", +//// "type": "module", +//// "exports": { +//// ".": { +//// "import": "./index.js" +//// } +//// } +//// } + +// @Filename: /node_modules/esm-only/index.d.ts +//// export declare const esm: any; + +// @Filename: /main.mts +//// import { esm } from "esm-only"; +//// [|esm.ohno;|] + +verify.moveToFile({ + newFileContents: { + "/main.mts":``, + + "/bar.cts": +`const a = 2; +esm.ohno; +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.cts" } +}); \ No newline at end of file diff --git a/tests/cases/fourslash/moveToFile_multipleStatements.ts b/tests/cases/fourslash/moveToFile_multipleStatements.ts new file mode 100644 index 0000000000000..af2454d59d280 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_multipleStatements.ts @@ -0,0 +1,36 @@ +/// + +//@Filename: /bar.ts +////import { } from './somefile'; +////const a = 12; + +// @Filename: /a.ts +////[|const q = 0; +////const t = 2; +////function f() {} +////class C {} +////enum E {} +////namespace N { export const h = 0; } +////type T = number; +////interface I {}|] + +verify.moveToFile({ + newFileContents: { + "/a.ts": +``, + + "/bar.ts": +`import { } from './somefile'; +const a = 12; +const q = 0; +const t = 2; +function f() { } +class C { } +enum E { } +namespace N { export const h = 0; } +type T = number; +interface I { } +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.ts" } +}); diff --git a/tests/cases/fourslash/moveToFile_namedImports.ts b/tests/cases/fourslash/moveToFile_namedImports.ts new file mode 100644 index 0000000000000..916831d7699fd --- /dev/null +++ b/tests/cases/fourslash/moveToFile_namedImports.ts @@ -0,0 +1,45 @@ + +/// + +// @Filename: /bar.ts +////const q = 0; + +// @Filename: /a.ts +////// header comment +//// +////import { } from './other'; +////import type { } from './other'; +//// +////export const p = 0; +////export const b = 1; +////[|const y = p + b;|] + +// @Filename: /other.ts +////export const t = 2; + + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`// header comment + +import { } from './other'; +import type { } from './other'; + +export const p = 0; +export const b = 1; +`, + + "/bar.ts": +`import { p, b } from './a'; + +const q = 0; +const y = p + b; +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.ts" }, + + preferences: { + quotePreference: "single", + } +}); diff --git a/tests/cases/fourslash/moveToFile_newFile.ts b/tests/cases/fourslash/moveToFile_newFile.ts new file mode 100644 index 0000000000000..ae8ed5bfe28dc --- /dev/null +++ b/tests/cases/fourslash/moveToFile_newFile.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////[|interface ka { +//// name: string; +////}|] + +verify.moveToFile({ + newFileContents: { + "/a.ts": +``, + + "/bar.ts": +`interface ka { + name: string; +} +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.ts" } +}); diff --git a/tests/cases/fourslash/moveToFile_noImportExport.ts b/tests/cases/fourslash/moveToFile_noImportExport.ts new file mode 100644 index 0000000000000..bd992fad71293 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_noImportExport.ts @@ -0,0 +1,27 @@ +/// + +//@Filename: /bar.ts +////const q = "test"; + +// @Filename: /a.ts +////const z = 1; +////[|const y = z + 2;|] +////const t = y + 1; + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`import { y } from "./bar"; + +export const z = 1; +const t = y + 1;`, + + "/bar.ts": +`import { z } from "./a"; + +const q = "test"; +export const y = z + 2; +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.ts" } +}); diff --git a/tests/cases/fourslash/moveToFile_nonExportedImports.ts b/tests/cases/fourslash/moveToFile_nonExportedImports.ts new file mode 100644 index 0000000000000..142505c3e2636 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_nonExportedImports.ts @@ -0,0 +1,32 @@ +/// + +//@Filename: /bar.ts +////import './someFile'; +//// +////const q = 20; + +// @Filename: /a.ts +////import { b } from './other'; +////const p = 0; +////[|const y: Date = p + b;|] + +// @Filename: /other.ts +////export const b = 2; + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`export const p = 0; +`, + + "/bar.ts": +`import { p } from './a'; +import { b } from './other'; +import './someFile'; + +const q = 20; +const y: Date = p + b; +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.ts" } +}); diff --git a/tests/cases/fourslash/moveToFile_requireImport.ts b/tests/cases/fourslash/moveToFile_requireImport.ts new file mode 100644 index 0000000000000..df6a294af2155 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_requireImport.ts @@ -0,0 +1,44 @@ +/// + +// @module: commonjs +// @allowJs: true + +//@Filename: /bar.js +////const x = 0; + +//@Filename: /a.js +////const { a, b } = require("./other"); +////const p = 0; +////[|const y = p; +////const z = 0; +////exports.z = 0;|] +////a; y; z; + +//@Filename: /other.js +////const a = 1; +////exports.a = a; + +//@Filename: /user.ts +////const { x, y } = require("./a"); + +verify.moveToFile({ + newFileContents: { + "/a.js": +`const { y, z } = require("./bar"); +const { a, b } = require("./other"); +const p = 0; +exports.p = p; +a; y; z;`, + + "/bar.js": +`const { p } = require("./a"); + +const x = 0; +const y = p; +exports.y = y; +const z = 0; +exports.z = 0; +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.js" } +}); diff --git a/tests/cases/fourslash/moveToFile_requireImport2.ts b/tests/cases/fourslash/moveToFile_requireImport2.ts new file mode 100644 index 0000000000000..a0e5ffd8226ad --- /dev/null +++ b/tests/cases/fourslash/moveToFile_requireImport2.ts @@ -0,0 +1,41 @@ +/// + +// @verbatimModuleSyntax: true +// @module: commonjs +// @allowJs: true + +//@Filename: /a.js +////const { a, b } = require("./other"); +////const p = 0; +////[|const y = p; +////const z = 0; +////exports.z = 0;|] +////a; y; z; + +//@Filename: /other.js +////const a = 1; +////exports.a = a; + +//@Filename: /user.ts +////const { x, y } = require("./a"); + +verify.moveToFile({ + newFileContents: { + "/a.js": +`const { y, z } = require("./bar"); +const { a, b } = require("./other"); +const p = 0; +exports.p = p; +a; y; z;`, + + "/bar.js": +`const { p } = require("./a"); + +const y = p; +exports.y = y; +const z = 0; +exports.z = 0; +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.js" } +}); diff --git a/tests/cases/fourslash/moveToFile_typeImport.ts b/tests/cases/fourslash/moveToFile_typeImport.ts new file mode 100644 index 0000000000000..61e1b2b48e2c0 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_typeImport.ts @@ -0,0 +1,35 @@ + +/// + +// @verbatimModuleSyntax: true +//@module: esnext +//moduleResolution: bundler + +// @Filename: /bar.ts +////import {} from "./somefile"; + +// @Filename: /a.ts +////import { type A } from "./other"; +////import type { B } from "./other"; +////[|function f(a: B) {}|] + +// @Filename: /other.ts +////export type B = {}; +////export interface A { +//// x: number; +////} + + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`import { type A } from "./other"; +`, + "/bar.ts": +`import type { B } from "./other"; +import {} from "./somefile"; +function f(a: B) { } +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.ts" } +}); diff --git a/tests/cases/fourslash/moveToFile_unresolvedImport.ts b/tests/cases/fourslash/moveToFile_unresolvedImport.ts new file mode 100644 index 0000000000000..8cdd873361f22 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_unresolvedImport.ts @@ -0,0 +1,23 @@ +/// + +// @Filename: /bar.ts +////const a = 1; + +// @Filename: /a.ts +////import { a } from 'doesnt-exist'; +////[|a();|] + +verify.moveToFile({ + newFileContents: { + "/a.ts": +``, + + "/bar.ts": +`import { a } from "doesnt-exist"; + +const a = 1; +a(); +`, + }, + interactiveRefactorArguments: { targetFile: "/bar.ts" } +}); \ No newline at end of file