diff --git a/src/services/completions.ts b/src/services/completions.ts index 81ff0dd582a..e3417b9e652 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -288,7 +288,7 @@ namespace ts.Completions { switch (completionData.kind) { case CompletionDataKind.Data: - const response = completionInfoFromData(sourceFile, host, program, compilerOptions, log, completionData, preferences, formatContext); + const response = completionInfoFromData(sourceFile, host, program, compilerOptions, log, completionData, preferences, formatContext, position); if (response?.isIncomplete) { incompleteCompletionsCache?.set(response); } @@ -452,6 +452,7 @@ namespace ts.Completions { completionData: CompletionData, preferences: UserPreferences, formatContext: formatting.FormatContext | undefined, + position: number ): CompletionInfo | undefined { const { symbols, @@ -512,7 +513,7 @@ namespace ts.Completions { isJsxIdentifierExpected, isRightOfOpenTag, ); - getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries); // TODO: GH#18217 + getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries); } else { if (!isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) { @@ -556,6 +557,13 @@ namespace ts.Completions { } } + const entryNames = new Set(entries.map(e => e.name)); + for (const keywordEntry of getContextualKeywords(contextToken, position)) { + if (!entryNames.has(keywordEntry.name)) { + insertSorted(entries, keywordEntry, compareCompletionEntries, /*allowDuplicates*/ true); + } + } + for (const literal of literals) { insertSorted(entries, createCompletionEntryForLiteral(sourceFile, preferences, literal), compareCompletionEntries, /*allowDuplicates*/ true); } @@ -3630,6 +3638,38 @@ namespace ts.Completions { return isIdentifier(node) ? node.originalKeywordKind || SyntaxKind.Unknown : node.kind; } + function getContextualKeywords( + contextToken: Node | undefined, + position: number, + ): readonly CompletionEntry[] { + const entries = []; + /** + * An `AssertClause` can come after an import declaration: + * import * from "foo" | + * import "foo" | + * or after a re-export declaration that has a module specifier: + * export { foo } from "foo" | + * Source: https://tc39.es/proposal-import-assertions/ + */ + if (contextToken) { + const file = contextToken.getSourceFile(); + const parent = contextToken.parent; + const tokenLine = file.getLineAndCharacterOfPosition(contextToken.end).line; + const currentLine = file.getLineAndCharacterOfPosition(position).line; + if ((isImportDeclaration(parent) || isExportDeclaration(parent) && parent.moduleSpecifier) + && contextToken === parent.moduleSpecifier + && tokenLine === currentLine) { + entries.push({ + name: tokenToString(SyntaxKind.AssertKeyword)!, + kind: ScriptElementKind.keyword, + kindModifiers: ScriptElementKindModifier.none, + sortText: SortText.GlobalsOrKeywords, + }); + } + } + return entries; + } + /** Get the corresponding JSDocTag node if the position is in a jsDoc comment */ function getJsDocTagAtPosition(node: Node, position: number): JSDocTag | undefined { return findAncestor(node, n => diff --git a/tests/cases/fourslash/completionsAssertKeyword.ts b/tests/cases/fourslash/completionsAssertKeyword.ts new file mode 100644 index 00000000000..daa02cd33b1 --- /dev/null +++ b/tests/cases/fourslash/completionsAssertKeyword.ts @@ -0,0 +1,54 @@ +/// + +// @allowJs: true +// @Filename: a.ts +//// const f = { +//// a: 1 +////}; +//// import * as thing from "thing" /*0*/ +//// export { foo } from "foo" /*1*/ +//// import "foo" as /*2*/ +//// import "foo" a/*3*/ +//// import * as that from "that" +//// /*4*/ +//// import * /*5*/ as those from "those" + +// @Filename: b.js +//// import * as thing from "thing" /*js*/; + +const assertEntry = { + name: "assert", + kind: "keyword", + sortText: completion.SortText.GlobalsOrKeywords, +}; + +verify.completions( + { + marker: "0", + includes: [assertEntry], + }, + { + marker: "1", + includes: [assertEntry], + }, + { + marker: "2", + excludes: ["assert"], + }, + { + marker: "3", + includes: [assertEntry], + }, + { + marker: "4", + excludes: ["assert"], + }, + { + marker: "5", + excludes: ["assert"], + }, + { + marker: "js", + includes: [assertEntry], + }, +);