diff --git a/src/services/completions.ts b/src/services/completions.ts index c5ddaeca0bf..4a907ed2fe1 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -602,6 +602,7 @@ namespace ts.Completions { propertyAccessToConvert: PropertyAccessExpression | undefined, isJsxInitializer: IsJsxInitializer | undefined, importCompletionNode: Node | undefined, + isTypeOnlyImport: boolean, useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences, @@ -661,7 +662,7 @@ namespace ts.Completions { if (originIsResolvedExport(origin)) { sourceDisplay = [textPart(origin.moduleSpecifier)]; if (importCompletionNode) { - ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, origin, useSemicolons, options, preferences)); + ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, isTypeOnlyImport, origin, useSemicolons, options, preferences)); isSnippet = preferences.includeCompletionsWithSnippetText ? true : undefined; } } @@ -746,7 +747,7 @@ namespace ts.Completions { }; } - function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences) { + function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, isTypeOnly: boolean | undefined, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences) { const sourceFile = importCompletionNode.getSourceFile(); const replacementSpan = createTextSpanFromNode(importCompletionNode, sourceFile); const quotedModuleSpecifier = quote(sourceFile, preferences, origin.moduleSpecifier); @@ -756,12 +757,13 @@ namespace ts.Completions { ExportKind.Named; const tabStop = preferences.includeCompletionsWithSnippetText ? "$1" : ""; const importKind = codefix.getImportKind(sourceFile, exportKind, options, /*forceImportKeyword*/ true); + const typeOnlyPrefix = isTypeOnly ? ` ${tokenToString(SyntaxKind.TypeKeyword)} ` : " "; const suffix = useSemicolons ? ";" : ""; switch (importKind) { - case ImportKind.CommonJS: return { replacementSpan, insertText: `import ${escapeSnippetText(name)}${tabStop} = require(${quotedModuleSpecifier})${suffix}` }; - case ImportKind.Default: return { replacementSpan, insertText: `import ${escapeSnippetText(name)}${tabStop} from ${quotedModuleSpecifier}${suffix}` }; - case ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${escapeSnippetText(name)} from ${quotedModuleSpecifier}${suffix}` }; - case ImportKind.Named: return { replacementSpan, insertText: `import { ${escapeSnippetText(name)}${tabStop} } from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.CommonJS: return { replacementSpan, insertText: `import${typeOnlyPrefix}${escapeSnippetText(name)}${tabStop} = require(${quotedModuleSpecifier})${suffix}` }; + case ImportKind.Default: return { replacementSpan, insertText: `import${typeOnlyPrefix}${escapeSnippetText(name)}${tabStop} from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.Namespace: return { replacementSpan, insertText: `import${typeOnlyPrefix}* as ${escapeSnippetText(name)} from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.Named: return { replacementSpan, insertText: `import${typeOnlyPrefix}{ ${escapeSnippetText(name)}${tabStop} } from ${quotedModuleSpecifier}${suffix}` }; } } @@ -814,6 +816,7 @@ namespace ts.Completions { const start = timestamp(); const variableDeclaration = getVariableDeclaration(location); const useSemicolons = probablyUsesSemicolons(sourceFile); + const isTypeOnlyImport = !!importCompletionNode && isTypeOnlyImportOrExportDeclaration(location.parent); // Tracks unique names. // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. @@ -844,6 +847,7 @@ namespace ts.Completions { propertyAccessToConvert, isJsxInitializer, importCompletionNode, + isTypeOnlyImport, useSemicolons, compilerOptions, preferences @@ -1916,6 +1920,7 @@ namespace ts.Completions { function isTypeOnlyCompletion(): boolean { return insideJsDocTagTypeExpression + || !!importCompletionNode && isTypeOnlyImportOrExportDeclaration(location.parent) || !isContextTokenValueLocation(contextToken) && (isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker) || isPartOfTypeNode(location) diff --git a/tests/cases/fourslash/importTypeCompletions1.ts b/tests/cases/fourslash/importTypeCompletions1.ts new file mode 100644 index 00000000000..d4bf3df2809 --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions1.ts @@ -0,0 +1,26 @@ +/// +// @target: esnext + +// @filename: /foo.ts +////export interface Foo {} + +// @filename: /bar.ts +////[|import type F/**/|] + +goTo.file("/bar.ts") +verify.completions({ + marker: "", + exact: [{ + name: "Foo", + sourceDisplay: "./foo", + source: "./foo", + insertText: "import type { Foo } from \"./foo\";", + replacementSpan: test.ranges()[0] + }], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +}); diff --git a/tests/cases/fourslash/importTypeCompletions2.ts b/tests/cases/fourslash/importTypeCompletions2.ts new file mode 100644 index 00000000000..db252b9f2e9 --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions2.ts @@ -0,0 +1,20 @@ +/// +// @target: esnext + +// @filename: /foo.ts +////export const Foo = {}; + +// @filename: /bar.ts +////[|import type F/**/|] + +goTo.file("/bar.ts") +verify.completions({ + marker: "", + exact: [], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +}); diff --git a/tests/cases/fourslash/importTypeCompletions3.ts b/tests/cases/fourslash/importTypeCompletions3.ts new file mode 100644 index 00000000000..56a242c7196 --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions3.ts @@ -0,0 +1,26 @@ +/// +// @target: esnext + +// @filename: /foo.ts +////export interface Foo {} + +// @filename: /bar.ts +////[|import type { F/**/ }|] + +goTo.file("/bar.ts") +verify.completions({ + marker: "", + exact: [{ + name: "Foo", + sourceDisplay: "./foo", + source: "./foo", + insertText: "import type { Foo } from \"./foo\";", + replacementSpan: test.ranges()[0] + }], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +}); diff --git a/tests/cases/fourslash/importTypeCompletions4.ts b/tests/cases/fourslash/importTypeCompletions4.ts new file mode 100644 index 00000000000..690107cd19c --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions4.ts @@ -0,0 +1,27 @@ +/// +// @esModuleInterop: true + +// @Filename: /foo.ts +////interface Foo { }; +////export = Foo; + +// @Filename: /bar.ts +//// [|import type f/**/|] + +goTo.file("/bar.ts") +verify.completions({ + marker: "", + exact: [{ + name: "Foo", + sourceDisplay: "./foo", + source: "./foo", + insertText: "import type Foo from \"./foo\";", + replacementSpan: test.ranges()[0] + }], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +}); diff --git a/tests/cases/fourslash/importTypeCompletions5.ts b/tests/cases/fourslash/importTypeCompletions5.ts new file mode 100644 index 00000000000..270a79adbe9 --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions5.ts @@ -0,0 +1,28 @@ +/// + +// @esModuleInterop: false + +// @Filename: /foo.ts +////interface Foo { }; +////export = Foo; + +// @Filename: /bar.ts +//// [|import type f/**/|] + +goTo.file("/bar.ts") +verify.completions({ + marker: "", + exact: [{ + name: "Foo", + sourceDisplay: "./foo", + source: "./foo", + insertText: "import type Foo = require(\"./foo\");", + replacementSpan: test.ranges()[0] + }], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +}); diff --git a/tests/cases/fourslash/importTypeCompletions6.ts b/tests/cases/fourslash/importTypeCompletions6.ts new file mode 100644 index 00000000000..f860ddfa665 --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions6.ts @@ -0,0 +1,28 @@ +/// + +// @module: esnext + +// @Filename: /foo.ts +////export const foo = { }; +////export interface Foo { }; + +// @Filename: /bar.ts +//// [|import type * as f/**/|] + +goTo.file("/bar.ts") +verify.completions({ + marker: "", + exact: [{ + name: "Foo", + sourceDisplay: "./foo", + source: "./foo", + insertText: "import type { Foo } from \"./foo\";", + replacementSpan: test.ranges()[0] + }], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +}); diff --git a/tests/cases/fourslash/importTypeCompletions7.ts b/tests/cases/fourslash/importTypeCompletions7.ts new file mode 100644 index 00000000000..85825b4ef50 --- /dev/null +++ b/tests/cases/fourslash/importTypeCompletions7.ts @@ -0,0 +1,29 @@ +/// + +// @target: es2020 +// @module: esnext + +// @Filename: /foo.d.ts +//// declare namespace Foo {} +//// export = Foo; + +// @Filename: /test.ts +//// [|import F/**/|] + +goTo.file("/test.ts") +verify.completions({ + marker: "", + exact: [{ + name: "Foo", + sourceDisplay: "./foo", + source: "./foo", + insertText: "import * as Foo from \"./foo\";", + replacementSpan: test.ranges()[0] + }], + isNewIdentifierLocation: true, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsForImportStatements: true, + includeCompletionsWithInsertText: true + } +});