diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 8f855347bd801..2309977539cb9 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3793,5 +3793,9 @@ "Infer parameter types from usage.": { "category": "Message", "code": 95012 + }, + "Convert to default import": { + "category": "Message", + "code": 95013 } } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 652737f9b57e6..4f1fcd4a06cec 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -470,6 +470,10 @@ namespace FourSlash { public select(startMarker: string, endMarker: string) { const start = this.getMarkerByName(startMarker), end = this.getMarkerByName(endMarker); + ts.Debug.assert(start.fileName === end.fileName); + if (this.activeFile.fileName !== start.fileName) { + this.openFile(start.fileName); + } this.goToPosition(start.position); this.selectionEnd = end.position; } diff --git a/src/services/refactors/refactors.ts b/src/services/refactors/refactors.ts index f4b56422a89bd..3858b1987434e 100644 --- a/src/services/refactors/refactors.ts +++ b/src/services/refactors/refactors.ts @@ -2,3 +2,4 @@ /// /// /// +/// diff --git a/src/services/refactors/useDefaultImport.ts b/src/services/refactors/useDefaultImport.ts new file mode 100644 index 0000000000000..56faf082a4952 --- /dev/null +++ b/src/services/refactors/useDefaultImport.ts @@ -0,0 +1,96 @@ +/* @internal */ +namespace ts.refactor.installTypesForPackage { + const actionName = "Convert to default import"; + + const useDefaultImport: Refactor = { + name: actionName, + description: getLocaleSpecificMessage(Diagnostics.Convert_to_default_import), + getEditsForAction, + getAvailableActions, + }; + + registerRefactor(useDefaultImport); + + function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { + const { file, startPosition, program } = context; + + if (!program.getCompilerOptions().allowSyntheticDefaultImports) { + return undefined; + } + + const importInfo = getConvertibleImportAtPosition(file, startPosition); + if (!importInfo) { + return undefined; + } + + const module = ts.getResolvedModule(file, importInfo.moduleSpecifier.text); + const resolvedFile = program.getSourceFile(module.resolvedFileName); + if (!(resolvedFile.externalModuleIndicator && isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals)) { + return undefined; + } + + return [ + { + name: useDefaultImport.name, + description: useDefaultImport.description, + actions: [ + { + description: useDefaultImport.description, + name: actionName, + }, + ], + }, + ]; + } + + function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined { + const { file, startPosition } = context; + Debug.assertEqual(actionName, _actionName); + const importInfo = getConvertibleImportAtPosition(file, startPosition); + if (!importInfo) { + return undefined; + } + const { importStatement, name, moduleSpecifier } = importInfo; + const newImportClause = createImportClause(name, /*namedBindings*/ undefined); + const newImportStatement = ts.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, newImportClause, moduleSpecifier); + return { + edits: textChanges.ChangeTracker.with(context, t => t.replaceNode(file, importStatement, newImportStatement)), + renameFilename: undefined, + renameLocation: undefined, + }; + } + + function getConvertibleImportAtPosition( + file: SourceFile, + startPosition: number, + ): { importStatement: AnyImportSyntax, name: Identifier, moduleSpecifier: StringLiteral } | undefined { + let node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false); + while (true) { + switch (node.kind) { + case SyntaxKind.ImportEqualsDeclaration: + const eq = node as ImportEqualsDeclaration; + const { moduleReference } = eq; + return moduleReference.kind === SyntaxKind.ExternalModuleReference && isStringLiteral(moduleReference.expression) + ? { importStatement: eq, name: eq.name, moduleSpecifier: moduleReference.expression } + : undefined; + case SyntaxKind.ImportDeclaration: + const d = node as ImportDeclaration; + const { importClause } = d; + return !importClause.name && importClause.namedBindings.kind === SyntaxKind.NamespaceImport && isStringLiteral(d.moduleSpecifier) + ? { importStatement: d, name: importClause.namedBindings.name, moduleSpecifier: d.moduleSpecifier } + : undefined; + // For known child node kinds of convertible imports, try again with parent node. + case SyntaxKind.NamespaceImport: + case SyntaxKind.ExternalModuleReference: + case SyntaxKind.ImportKeyword: + case SyntaxKind.Identifier: + case SyntaxKind.StringLiteral: + case SyntaxKind.AsteriskToken: + break; + default: + return undefined; + } + node = node.parent; + } + } +} diff --git a/tests/cases/fourslash/refactorUseDefaultImport.ts b/tests/cases/fourslash/refactorUseDefaultImport.ts new file mode 100644 index 0000000000000..8834b70f85e64 --- /dev/null +++ b/tests/cases/fourslash/refactorUseDefaultImport.ts @@ -0,0 +1,29 @@ +/// + +// @allowSyntheticDefaultImports: true + +// @Filename: /a.d.ts +////declare const x: number; +////export = x; + +// @Filename: /b.ts +/////*b0*/import * as a from "./a";/*b1*/ + +// @Filename: /c.ts +/////*c0*/import a = require("./a");/*c1*/ + +goTo.select("b0", "b1"); +edit.applyRefactor({ + refactorName: "Convert to default import", + actionName: "Convert to default import", + actionDescription: "Convert to default import", + newContent: 'import a from "./a";', +}); + +goTo.select("c0", "c1"); +edit.applyRefactor({ + refactorName: "Convert to default import", + actionName: "Convert to default import", + actionDescription: "Convert to default import", + newContent: 'import a from "./a";', +});