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";',
+});