diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 941803ea465b1..3ffacdb0abbb2 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -41414,7 +41414,7 @@ namespace ts {
}
else {
Debug.assert(node.kind !== SyntaxKind.VariableDeclaration);
- const importDeclaration = findAncestor(node, or(isImportDeclaration, isImportEqualsDeclaration)) as ImportDeclaration | ImportEqualsDeclaration | undefined;
+ const importDeclaration = findAncestor(node, or(isImportDeclaration, isImportEqualsDeclaration));
const moduleSpecifier = (importDeclaration && tryGetModuleSpecifierFromDeclaration(importDeclaration)?.text) ?? "...";
const importedIdentifier = unescapeLeadingUnderscores(isIdentifier(errorNode) ? errorNode.escapedText : symbol.escapedName);
error(
diff --git a/src/compiler/core.ts b/src/compiler/core.ts
index 934f5ace84990..698f7bb2fb705 100644
--- a/src/compiler/core.ts
+++ b/src/compiler/core.ts
@@ -2371,6 +2371,9 @@ namespace ts {
return (arg: T) => f(arg) && g(arg);
}
+ export function or
(f1: (p1: P) => p1 is R1, f2: (p2: P) => p2 is R2): (p: P) => p is R1 | R2;
+ export function or
(f1: (p1: P) => p1 is R1, f2: (p2: P) => p2 is R2, f3: (p3: P) => p3 is R3): (p: P) => p is R1 | R2 | R3;
+ export function or(...fs: ((...args: T) => U)[]): (...args: T) => U;
export function or(...fs: ((...args: T) => U)[]): (...args: T) => U {
return (...args) => {
let lastResult: U;
diff --git a/src/services/completions.ts b/src/services/completions.ts
index afea634c32b2c..bff90e2d3e7c5 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -492,7 +492,7 @@ namespace ts.Completions {
isTypeOnlyLocation,
isJsxIdentifierExpected,
isRightOfOpenTag,
- importCompletionNode,
+ importStatementCompletion,
insideJsDocTagTypeExpression,
symbolToSortTextMap: symbolToSortTextMap,
hasUnresolvedAutoImports,
@@ -530,7 +530,7 @@ namespace ts.Completions {
propertyAccessToConvert,
isJsxIdentifierExpected,
isJsxInitializer,
- importCompletionNode,
+ importStatementCompletion,
recommendedCompletion,
symbolToOriginInfoMap,
symbolToSortTextMap,
@@ -685,7 +685,7 @@ namespace ts.Completions {
recommendedCompletion: Symbol | undefined,
propertyAccessToConvert: PropertyAccessExpression | undefined,
isJsxInitializer: IsJsxInitializer | undefined,
- importCompletionNode: Node | undefined,
+ importStatementCompletion: ImportStatementCompletionInfo | undefined,
useSemicolons: boolean,
options: CompilerOptions,
preferences: UserPreferences,
@@ -751,8 +751,8 @@ namespace ts.Completions {
if (originIsResolvedExport(origin)) {
sourceDisplay = [textPart(origin.moduleSpecifier)];
- if (importCompletionNode) {
- ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, contextToken, origin, useSemicolons, options, preferences));
+ if (importStatementCompletion) {
+ ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importStatementCompletion, origin, useSemicolons, sourceFile, options, preferences));
isSnippet = preferences.includeCompletionsWithSnippetText ? true : undefined;
}
}
@@ -816,7 +816,7 @@ namespace ts.Completions {
if (originIsExport(origin) || originIsResolvedExport(origin)) {
data = originToCompletionEntryData(origin);
- hasAction = !importCompletionNode;
+ hasAction = !importStatementCompletion;
}
// TODO(drosen): Right now we just permit *all* semantic meanings when calling
@@ -841,7 +841,7 @@ namespace ts.Completions {
labelDetails,
isSnippet,
isPackageJsonImport: originIsPackageJsonImport(origin) || undefined,
- isImportStatementCompletion: !!importCompletionNode || undefined,
+ isImportStatementCompletion: !!importStatementCompletion || undefined,
data,
};
}
@@ -1338,9 +1338,8 @@ namespace ts.Completions {
return unresolvedOrigin;
}
- function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, contextToken: Node | undefined, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences) {
- const sourceFile = importCompletionNode.getSourceFile();
- const replacementSpan = createTextSpanFromNode(findAncestor(importCompletionNode, or(isImportDeclaration, isImportEqualsDeclaration)) || importCompletionNode, sourceFile);
+ function getInsertTextAndReplacementSpanForImportCompletion(name: string, importStatementCompletion: ImportStatementCompletionInfo, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, sourceFile: SourceFile, options: CompilerOptions, preferences: UserPreferences) {
+ const replacementSpan = importStatementCompletion.replacementSpan;
const quotedModuleSpecifier = quote(sourceFile, preferences, origin.moduleSpecifier);
const exportKind =
origin.isDefaultExport ? ExportKind.Default :
@@ -1348,9 +1347,8 @@ namespace ts.Completions {
ExportKind.Named;
const tabStop = preferences.includeCompletionsWithSnippetText ? "$1" : "";
const importKind = codefix.getImportKind(sourceFile, exportKind, options, /*forceImportKeyword*/ true);
- const isTopLevelTypeOnly = tryCast(importCompletionNode, isImportDeclaration)?.importClause?.isTypeOnly || tryCast(importCompletionNode, isImportEqualsDeclaration)?.isTypeOnly;
- const isImportSpecifierTypeOnly = couldBeTypeOnlyImportSpecifier(importCompletionNode, contextToken);
- const topLevelTypeOnlyText = isTopLevelTypeOnly ? ` ${tokenToString(SyntaxKind.TypeKeyword)} ` : " ";
+ const isImportSpecifierTypeOnly = importStatementCompletion.couldBeTypeOnlyImportSpecifier;
+ const topLevelTypeOnlyText = importStatementCompletion.isTopLevelTypeOnly ? ` ${tokenToString(SyntaxKind.TypeKeyword)} ` : " ";
const importSpecifierTypeOnlyText = isImportSpecifierTypeOnly ? `${tokenToString(SyntaxKind.TypeKeyword)} ` : "";
const suffix = useSemicolons ? ";" : "";
switch (importKind) {
@@ -1408,7 +1406,7 @@ namespace ts.Completions {
propertyAccessToConvert?: PropertyAccessExpression,
jsxIdentifierExpected?: boolean,
isJsxInitializer?: IsJsxInitializer,
- importCompletionNode?: Node,
+ importStatementCompletion?: ImportStatementCompletionInfo,
recommendedCompletion?: Symbol,
symbolToOriginInfoMap?: SymbolOriginInfoMap,
symbolToSortTextMap?: SymbolSortTextMap,
@@ -1450,7 +1448,7 @@ namespace ts.Completions {
recommendedCompletion,
propertyAccessToConvert,
isJsxInitializer,
- importCompletionNode,
+ importStatementCompletion,
useSemicolons,
compilerOptions,
preferences,
@@ -1725,7 +1723,7 @@ namespace ts.Completions {
cancellationToken: CancellationToken,
): CodeActionsAndSourceDisplay {
if (data?.moduleSpecifier) {
- if (previousToken && getImportStatementCompletionInfo(contextToken || previousToken).replacementNode) {
+ if (previousToken && getImportStatementCompletionInfo(contextToken || previousToken).replacementSpan) {
// Import statement completion: 'import c|'
return { codeActions: undefined, sourceDisplay: [textPart(data.moduleSpecifier)] };
}
@@ -1831,7 +1829,7 @@ namespace ts.Completions {
/** In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. */
readonly isJsxIdentifierExpected: boolean;
readonly isRightOfOpenTag: boolean;
- readonly importCompletionNode?: Node;
+ readonly importStatementCompletion?: ImportStatementCompletionInfo;
readonly hasUnresolvedAutoImports?: boolean;
readonly flags: CompletionInfoFlags;
}
@@ -2014,35 +2012,35 @@ namespace ts.Completions {
let isStartingCloseTag = false;
let isJsxInitializer: IsJsxInitializer = false;
let isJsxIdentifierExpected = false;
- let importCompletionNode: Node | undefined;
+ let importStatementCompletion: ImportStatementCompletionInfo | undefined;
let location = getTouchingPropertyName(sourceFile, position);
let keywordFilters = KeywordCompletionFilters.None;
let isNewIdentifierLocation = false;
let flags = CompletionInfoFlags.None;
if (contextToken) {
- const importStatementCompletion = getImportStatementCompletionInfo(contextToken);
- isNewIdentifierLocation = importStatementCompletion.isNewIdentifierLocation;
- if (importStatementCompletion.keywordCompletion) {
- if (importStatementCompletion.isKeywordOnlyCompletion) {
+ const importStatementCompletionInfo = getImportStatementCompletionInfo(contextToken);
+ if (importStatementCompletionInfo.keywordCompletion) {
+ if (importStatementCompletionInfo.isKeywordOnlyCompletion) {
return {
kind: CompletionDataKind.Keywords,
- keywordCompletions: [keywordToCompletionEntry(importStatementCompletion.keywordCompletion)],
- isNewIdentifierLocation,
+ keywordCompletions: [keywordToCompletionEntry(importStatementCompletionInfo.keywordCompletion)],
+ isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation,
};
}
- keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletion.keywordCompletion);
+ keywordFilters = keywordFiltersFromSyntaxKind(importStatementCompletionInfo.keywordCompletion);
}
- if (importStatementCompletion.replacementNode && preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) {
+ if (importStatementCompletionInfo.replacementSpan && preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) {
// Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier`
// added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature
// is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients
// to opt in with the `includeCompletionsForImportStatements` user preference.
- importCompletionNode = importStatementCompletion.replacementNode;
flags |= CompletionInfoFlags.IsImportStatementCompletion;
+ importStatementCompletion = importStatementCompletionInfo;
+ isNewIdentifierLocation = importStatementCompletionInfo.isNewIdentifierLocation;
}
// Bail out if this is a known invalid completion location
- if (!importCompletionNode && isCompletionListBlocker(contextToken)) {
+ if (!importStatementCompletionInfo.replacementSpan && isCompletionListBlocker(contextToken)) {
log("Returning an empty list because completion was requested in an invalid position.");
return keywordFilters
? keywordCompletionData(keywordFilters, isJsOnlyLocation, isNewIdentifierDefinitionLocation())
@@ -2089,7 +2087,7 @@ namespace ts.Completions {
return undefined;
}
}
- else if (!importCompletionNode && sourceFile.languageVariant === LanguageVariant.JSX) {
+ else if (!importStatementCompletion) {
//
// If the tagname is a property access expression, we will then walk up to the top most of property access expression.
// Then, try to get a JSX container and its associated attributes type.
@@ -2246,7 +2244,7 @@ namespace ts.Completions {
isTypeOnlyLocation,
isJsxIdentifierExpected,
isRightOfOpenTag,
- importCompletionNode,
+ importStatementCompletion,
hasUnresolvedAutoImports,
flags,
};
@@ -2522,7 +2520,7 @@ namespace ts.Completions {
}
function tryGetImportCompletionSymbols(): GlobalsSearch {
- if (!importCompletionNode) return GlobalsSearch.Continue;
+ if (!importStatementCompletion) return GlobalsSearch.Continue;
isNewIdentifierLocation = true;
collectAutoImports();
return GlobalsSearch.Success;
@@ -2611,7 +2609,7 @@ namespace ts.Completions {
function shouldOfferImportCompletions(): boolean {
// If already typing an import statement, provide completions for it.
- if (importCompletionNode) return true;
+ if (importStatementCompletion) return true;
// If current completion is for non-contextual Object literal shortahands, ignore auto-import symbols
if (isNonContextualObjectLiteral) return false;
// If not already a module, must have modules enabled.
@@ -2638,7 +2636,7 @@ namespace ts.Completions {
function isTypeOnlyCompletion(): boolean {
return insideJsDocTagTypeExpression
- || !!importCompletionNode && isTypeOnlyImportOrExportDeclaration(location.parent)
+ || !!importStatementCompletion && isTypeOnlyImportOrExportDeclaration(location.parent)
|| !isContextTokenValueLocation(contextToken) &&
(isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker)
|| isPartOfTypeNode(location)
@@ -2695,8 +2693,7 @@ namespace ts.Completions {
flags |= CompletionInfoFlags.MayIncludeAutoImports;
// import { type | -> token text should be blank
const isAfterTypeOnlyImportSpecifierModifier = previousToken === contextToken
- && importCompletionNode
- && couldBeTypeOnlyImportSpecifier(importCompletionNode, contextToken);
+ && importStatementCompletion;
const lowerCaseTokenText =
isAfterTypeOnlyImportSpecifierModifier ? "" :
@@ -2714,7 +2711,7 @@ namespace ts.Completions {
program,
position,
preferences,
- !!importCompletionNode,
+ !!importStatementCompletion,
isValidTypeOnlyAliasUseSite(location),
context => {
exportInfo.search(
@@ -2723,7 +2720,7 @@ namespace ts.Completions {
(symbolName, targetFlags) => {
if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return false;
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return false;
- if (!isTypeOnlyLocation && !importCompletionNode && !(targetFlags & SymbolFlags.Value)) return false;
+ if (!isTypeOnlyLocation && !importStatementCompletion && !(targetFlags & SymbolFlags.Value)) return false;
if (isTypeOnlyLocation && !(targetFlags & (SymbolFlags.Module | SymbolFlags.Type))) return false;
// Do not try to auto-import something with a lowercase first letter for a JSX tag
const firstChar = symbolName.charCodeAt(0);
@@ -2817,7 +2814,7 @@ namespace ts.Completions {
return;
}
symbolToOriginInfoMap[symbols.length] = origin;
- symbolToSortTextMap[symbolId] = importCompletionNode ? SortText.LocationPriority : SortText.AutoImportSuggestions;
+ symbolToSortTextMap[symbolId] = importStatementCompletion ? SortText.LocationPriority : SortText.AutoImportSuggestions;
symbols.push(symbol);
}
@@ -4248,7 +4245,9 @@ namespace ts.Completions {
isKeywordOnlyCompletion: boolean;
keywordCompletion: TokenSyntaxKind | undefined;
isNewIdentifierLocation: boolean;
- replacementNode: ImportEqualsDeclaration | ImportDeclaration | ImportSpecifier | Token | undefined;
+ isTopLevelTypeOnly: boolean;
+ couldBeTypeOnlyImportSpecifier: boolean;
+ replacementSpan: TextSpan | undefined;
}
function getImportStatementCompletionInfo(contextToken: Node): ImportStatementCompletionInfo {
@@ -4259,9 +4258,9 @@ namespace ts.Completions {
isKeywordOnlyCompletion,
keywordCompletion,
isNewIdentifierLocation: !!(candidate || keywordCompletion === SyntaxKind.TypeKeyword),
- replacementNode: candidate && rangeIsOnSingleLine(candidate, candidate.getSourceFile())
- ? candidate
- : undefined
+ isTopLevelTypeOnly: !!tryCast(candidate, isImportDeclaration)?.importClause?.isTypeOnly || !!tryCast(candidate, isImportEqualsDeclaration)?.isTypeOnly,
+ couldBeTypeOnlyImportSpecifier: !!candidate && couldBeTypeOnlyImportSpecifier(candidate, contextToken),
+ replacementSpan: getSingleLineReplacementSpanForImportCompletionNode(candidate),
};
function getCandidate() {
@@ -4308,15 +4307,70 @@ namespace ts.Completions {
}
}
+ function getSingleLineReplacementSpanForImportCompletionNode(node: ImportDeclaration | ImportEqualsDeclaration | ImportSpecifier | Token | undefined) {
+ if (!node) return undefined;
+ const top = findAncestor(node, or(isImportDeclaration, isImportEqualsDeclaration)) ?? node;
+ const sourceFile = top.getSourceFile();
+ if (rangeIsOnSingleLine(top, sourceFile)) {
+ return createTextSpanFromNode(top, sourceFile);
+ }
+ // ImportKeyword was necessarily on one line; ImportSpecifier was necessarily parented in an ImportDeclaration
+ Debug.assert(top.kind !== SyntaxKind.ImportKeyword && top.kind !== SyntaxKind.ImportSpecifier);
+ // Guess which point in the import might actually be a later statement parsed as part of the import
+ // during parser recovery - either in the middle of named imports, or the module specifier.
+ const potentialSplitPoint = top.kind === SyntaxKind.ImportDeclaration
+ ? getPotentiallyInvalidImportSpecifier(top.importClause?.namedBindings) ?? top.moduleSpecifier
+ : top.moduleReference;
+ const withoutModuleSpecifier: TextRange = {
+ pos: top.getFirstToken()!.getStart(),
+ end: potentialSplitPoint.pos,
+ };
+ // The module specifier/reference was previously found to be missing, empty, or
+ // not a string literal - in this last case, it's likely that statement on a following
+ // line was parsed as the module specifier of a partially-typed import, e.g.
+ // import Foo|
+ // interface Blah {}
+ // This appears to be a multiline-import, and editors can't replace multiple lines.
+ // But if everything but the "module specifier" is on one line, by this point we can
+ // assume that the "module specifier" is actually just another statement, and return
+ // the single-line range of the import excluding that probable statement.
+ if (rangeIsOnSingleLine(withoutModuleSpecifier, sourceFile)) {
+ return createTextSpanFromRange(withoutModuleSpecifier);
+ }
+ }
+
+ // Tries to identify the first named import that is not really a named import, but rather
+ // just parser recovery for a situation like:
+ // import { Foo|
+ // interface Bar {}
+ // in which `Foo`, `interface`, and `Bar` are all parsed as import specifiers. The caller
+ // will also check if this token is on a separate line from the rest of the import.
+ function getPotentiallyInvalidImportSpecifier(namedBindings: NamedImportBindings | undefined) {
+ return find(
+ tryCast(namedBindings, isNamedImports)?.elements,
+ e => !e.propertyName &&
+ isStringANonContextualKeyword(e.name.text) &&
+ findPrecedingToken(e.name.pos, namedBindings!.getSourceFile(), namedBindings)?.kind !== SyntaxKind.CommaToken);
+ }
+
function couldBeTypeOnlyImportSpecifier(importSpecifier: Node, contextToken: Node | undefined): importSpecifier is ImportSpecifier {
return isImportSpecifier(importSpecifier)
&& (importSpecifier.isTypeOnly || contextToken === importSpecifier.name && isTypeKeywordTokenOrIdentifier(contextToken));
}
function canCompleteFromNamedBindings(namedBindings: NamedImportBindings) {
- return isModuleSpecifierMissingOrEmpty(namedBindings.parent.parent.moduleSpecifier)
- && (isNamespaceImport(namedBindings) || namedBindings.elements.length < 2)
- && !namedBindings.parent.name;
+ if (!isModuleSpecifierMissingOrEmpty(namedBindings.parent.parent.moduleSpecifier) || namedBindings.parent.name) {
+ return false;
+ }
+ if (isNamedImports(namedBindings)) {
+ // We can only complete on named imports if there are no other named imports already,
+ // but parser recovery sometimes puts later statements in the named imports list, so
+ // we try to only consider the probably-valid ones.
+ const invalidNamedImport = getPotentiallyInvalidImportSpecifier(namedBindings);
+ const validImports = invalidNamedImport ? namedBindings.elements.indexOf(invalidNamedImport) : namedBindings.elements.length;
+ return validImports < 2;
+ }
+ return true;
}
function isModuleSpecifierMissingOrEmpty(specifier: ModuleReference | Expression) {
diff --git a/tests/cases/fourslash/importStatementCompletions1.ts b/tests/cases/fourslash/importStatementCompletions1.ts
index 89bc68189f9ee..0e970254346a5 100644
--- a/tests/cases/fourslash/importStatementCompletions1.ts
+++ b/tests/cases/fourslash/importStatementCompletions1.ts
@@ -22,37 +22,37 @@
// @Filename: /index5.ts
//// import f/*5*/ from "";
-// ([[0, true], [1, true], [2, false], [3, true], [4, true], [5, true]] as const).forEach(([marker, typeKeywordValid]) => {
-// verify.completions({
-// isNewIdentifierLocation: true,
-// marker: "" + marker,
-// exact: [{
-// name: "foo",
-// source: "./mod",
-// insertText: `import { foo$1 } from "./mod";`,
-// isSnippet: true,
-// replacementSpan: test.ranges()[marker],
-// sourceDisplay: "./mod",
-// },
-// {
-// name: "Foo",
-// source: "./mod",
-// insertText: `import { Foo$1 } from "./mod";`,
-// isSnippet: true,
-// replacementSpan: test.ranges()[marker],
-// sourceDisplay: "./mod",
-// },
-// ...typeKeywordValid ? [{
-// name: "type",
-// sortText: completion.SortText.GlobalsOrKeywords,
-// }] : []],
-// preferences: {
-// includeCompletionsForImportStatements: true,
-// includeInsertTextCompletions: true,
-// includeCompletionsWithSnippetText: true,
-// }
-// });
-// });
+([[0, true], [1, true], [2, false], [3, true], [4, true], [5, true]] as const).forEach(([marker, typeKeywordValid]) => {
+ verify.completions({
+ isNewIdentifierLocation: true,
+ marker: "" + marker,
+ exact: [{
+ name: "foo",
+ source: "./mod",
+ insertText: `import { foo$1 } from "./mod";`,
+ isSnippet: true,
+ replacementSpan: test.ranges()[marker],
+ sourceDisplay: "./mod",
+ },
+ {
+ name: "Foo",
+ source: "./mod",
+ insertText: `import { Foo$1 } from "./mod";`,
+ isSnippet: true,
+ replacementSpan: test.ranges()[marker],
+ sourceDisplay: "./mod",
+ },
+ ...typeKeywordValid ? [{
+ name: "type",
+ sortText: completion.SortText.GlobalsOrKeywords,
+ }] : []],
+ preferences: {
+ includeCompletionsForImportStatements: true,
+ includeInsertTextCompletions: true,
+ includeCompletionsWithSnippetText: true,
+ }
+ });
+});
// @Filename: /index6.ts
//// import f/*6*/ from "nope";
diff --git a/tests/cases/fourslash/importStatementCompletions2.ts b/tests/cases/fourslash/importStatementCompletions2.ts
new file mode 100644
index 0000000000000..05e668263c3c9
--- /dev/null
+++ b/tests/cases/fourslash/importStatementCompletions2.ts
@@ -0,0 +1,46 @@
+///
+
+// @Filename: /button.ts
+//// export function Button() {}
+
+// @Filename: /index1.ts
+//// [|import Butt/*0*/|]
+////
+//// interface Props {}
+//// const x = 0
+
+// @Filename: /index2.ts
+//// [|import { Butt/*1*/ }|]
+////
+//// interface Props {}
+//// const x = 0
+
+// @Filename: /index3.ts
+//// [|import { Butt/*2*/|]
+////
+//// interface Props {}
+//// const x = 0
+
+[0, 1, 2].forEach(marker => {
+ verify.completions({
+ marker: `${marker}`,
+ isNewIdentifierLocation: true,
+ exact: [{
+ name: "Button",
+ source: "./button",
+ sourceDisplay: "./button",
+ insertText: `import { Button$1 } from "./button"`,
+ isSnippet: true,
+ replacementSpan: test.ranges()[marker],
+ }, {
+ name: "type",
+ sortText: completion.SortText.GlobalsOrKeywords,
+ }],
+ preferences: {
+ includeCompletionsForImportStatements: true,
+ includeCompletionsForModuleExports: true,
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ },
+ });
+});