diff --git a/src/compiler/factory/nodeTests.ts b/src/compiler/factory/nodeTests.ts index 14e296610c54f..59525500a6c19 100644 --- a/src/compiler/factory/nodeTests.ts +++ b/src/compiler/factory/nodeTests.ts @@ -23,6 +23,7 @@ import { CallSignatureDeclaration, CaseBlock, CaseClause, + CaseKeyword, CatchClause, ClassDeclaration, ClassExpression, @@ -379,6 +380,11 @@ export function isImportKeyword(node: Node): node is ImportExpression { return node.kind === SyntaxKind.ImportKeyword; } +/** @internal */ +export function isCaseKeyword(node: Node): node is CaseKeyword { + return node.kind === SyntaxKind.CaseKeyword; +} + // Names export function isQualifiedName(node: Node): node is QualifiedName { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 7ce54d3dd6c02..28ce587a04957 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1344,6 +1344,7 @@ export interface KeywordToken extends Token; export type AssertKeyword = KeywordToken; export type AwaitKeyword = KeywordToken; +export type CaseKeyword = KeywordToken; /** @deprecated Use `AwaitKeyword` instead. */ export type AwaitKeywordToken = AwaitKeyword; diff --git a/src/services/completions.ts b/src/services/completions.ts index a09aebcb7d1a3..d2f33ae70e4a6 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -127,6 +127,7 @@ import { isCallExpression, isCaseBlock, isCaseClause, + isCaseKeyword, isCheckJsEnabledForFile, isClassElement, isClassLike, @@ -191,6 +192,7 @@ import { isNamedImports, isNamedImportsOrExports, isNamespaceImport, + isNodeDescendantOf, isObjectBindingPattern, isObjectLiteralExpression, isObjectTypeDeclaration, @@ -429,6 +431,7 @@ export const enum SymbolOriginInfoKind { ResolvedExport = 1 << 5, TypeOnlyAlias = 1 << 6, ObjectLiteralMethod = 1 << 7, + Ignore = 1 << 8, SymbolMemberNoExport = SymbolMember, SymbolMemberExport = SymbolMember | Export, @@ -507,6 +510,10 @@ function originIsObjectLiteralMethod(origin: SymbolOriginInfo | undefined): orig return !!(origin && origin.kind & SymbolOriginInfoKind.ObjectLiteralMethod); } +function originIsIgnore(origin: SymbolOriginInfo | undefined): boolean { + return !!(origin && origin.kind & SymbolOriginInfoKind.Ignore); +} + /** @internal */ export interface UniqueNameSet { add(name: string): void; @@ -859,7 +866,6 @@ function completionInfoFromData( location, propertyAccessToConvert, keywordFilters, - literals, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer, @@ -868,9 +874,12 @@ function completionInfoFromData( isRightOfOpenTag, importStatementCompletion, insideJsDocTagTypeExpression, - symbolToSortTextMap: symbolToSortTextMap, + symbolToSortTextMap, hasUnresolvedAutoImports, } = completionData; + let literals = completionData.literals; + + const checker = program.getTypeChecker(); // Verify if the file is JSX language variant if (getLanguageVariant(sourceFile.scriptKind) === LanguageVariant.JSX) { @@ -880,6 +889,25 @@ function completionInfoFromData( } } + // When the completion is for the expression of a case clause (e.g. `case |`), + // filter literals & enum symbols whose values are already present in existing case clauses. + const caseClause = findAncestor(contextToken, isCaseClause); + if (caseClause && (isCaseKeyword(contextToken!) || isNodeDescendantOf(contextToken!, caseClause.expression))) { + const tracker = newCaseClauseTracker(checker, caseClause.parent.clauses); + literals = literals.filter(literal => !tracker.hasValue(literal)); + // The `symbols` array cannot be filtered directly, because to each symbol at position i in `symbols`, + // there might be a corresponding origin at position i in `symbolToOriginInfoMap`. + // So instead of filtering the `symbols` array, we mark symbols to be ignored. + symbols.forEach((symbol, i) => { + if (symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) { + const value = checker.getConstantValue(symbol.valueDeclaration); + if (value !== undefined && tracker.hasValue(value)) { + symbolToOriginInfoMap[i] = { kind: SymbolOriginInfoKind.Ignore }; + } + } + }); + } + const entries = createSortedArray(); const isChecked = isCheckedFile(sourceFile, compilerOptions); if (isChecked && !isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) { @@ -4509,6 +4537,9 @@ function getCompletionEntryDisplayNameForSymbol( kind: CompletionKind, jsxIdentifierExpected: boolean, ): CompletionEntryDisplayNameForSymbol | undefined { + if (originIsIgnore(origin)) { + return undefined; + } const name = originIncludesSymbolName(origin) ? origin.symbolName : symbol.name; if (name === undefined // If the symbol is external module, don't show it in the completion list diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index e3e8466ccd1e7..16991df4f7ca7 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4547,6 +4547,7 @@ declare namespace ts { type AssertsKeyword = KeywordToken; type AssertKeyword = KeywordToken; type AwaitKeyword = KeywordToken; + type CaseKeyword = KeywordToken; /** @deprecated Use `AwaitKeyword` instead. */ type AwaitKeywordToken = AwaitKeyword; /** @deprecated Use `AssertsKeyword` instead. */ diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 7bc06c330da8a..7f8076812c4ab 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -611,6 +611,7 @@ declare namespace ts { type AssertsKeyword = KeywordToken; type AssertKeyword = KeywordToken; type AwaitKeyword = KeywordToken; + type CaseKeyword = KeywordToken; /** @deprecated Use `AwaitKeyword` instead. */ type AwaitKeywordToken = AwaitKeyword; /** @deprecated Use `AssertsKeyword` instead. */ diff --git a/tests/cases/fourslash/switchCompletions.ts b/tests/cases/fourslash/switchCompletions.ts new file mode 100644 index 0000000000000..425ae9c89451f --- /dev/null +++ b/tests/cases/fourslash/switchCompletions.ts @@ -0,0 +1,30 @@ +/// + +//// enum E { A, B } +//// declare const e: E; +//// switch (e) { +//// case E.A: +//// return 0; +//// case E./*1*/ +//// } +//// declare const f: 1 | 2 | 3; +//// switch (f) { +//// case 1: +//// return 1; +//// case /*2*/ +//// } + +verify.completions( + { + marker: "1", + isNewIdentifierLocation: false, + includes: ["B"], + excludes: "A", + }, + { + marker: "2", + isNewIdentifierLocation: false, + excludes: "1", + includes: ["2", "3"], + } +); \ No newline at end of file