Exhaustive case completion for switch statements (#50996)

* fix services' type's isLiteral

* update literal completions tests

* initial prototype

* use symbol to expression. TODO: filter existing, replace import nodes

* WIP

* WIP

* remove booleans from literals

* trigger at case keyword positions

* clean up tests

* fix element access expression case

* refactor dealing with existing values into a tracker

* fix merge errors

* cleanup and more tests

* fix lint errors

* more merge conflict fixes and cleanup

* use appropriate quotes

* small indentation fix

* refactor case clause tracker

* experiment: support tabstops after each case clause

* address small CR comments

* fix completion entry details; add test case

* fix lint errors

* remove space before tab stops; refactor
This commit is contained in:
Gabriela Araujo Britto 2022-12-01 21:48:32 -03:00 committed by GitHub
parent 5435efbf37
commit 6a3c9ea125
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 925 additions and 42 deletions

View File

@ -52,6 +52,7 @@ import {
canHaveIllegalDecorators,
canHaveIllegalModifiers,
canHaveModifiers,
canUsePropertyAccess,
cartesianProduct,
CaseBlock,
CaseClause,
@ -102,7 +103,6 @@ import {
createModeAwareCacheKey,
createPrinter,
createPropertyNameNodeForIdentifierOrLiteral,
createScanner,
createSymbolTable,
createTextWriter,
createUnderscoreEscapedMultiMap,
@ -500,7 +500,6 @@ import {
isGlobalScopeAugmentation,
isHeritageClause,
isIdentifier,
isIdentifierStart,
isIdentifierText,
isIdentifierTypePredicate,
isIdentifierTypeReference,
@ -678,6 +677,7 @@ import {
isTypeReferenceNode,
isTypeReferenceType,
isUMDExportSymbol,
isValidBigIntString,
isValidESSymbolDeclaration,
isValidTypeOnlyAliasUseSite,
isValueSignatureDeclaration,
@ -827,6 +827,7 @@ import {
parseIsolatedEntityName,
parseNodeFactory,
parsePseudoBigInt,
parseValidBigInt,
Path,
pathIsRelative,
PatternAmbientModule,
@ -7691,10 +7692,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (isSingleOrDoubleQuote(firstChar) && some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) {
return factory.createStringLiteral(getSpecifierForModuleSymbol(symbol, context));
}
const canUsePropertyAccess = firstChar === CharacterCodes.hash ?
symbolName.length > 1 && isIdentifierStart(symbolName.charCodeAt(1), languageVersion) :
isIdentifierStart(firstChar, languageVersion);
if (index === 0 || canUsePropertyAccess) {
if (index === 0 || canUsePropertyAccess(symbolName, languageVersion)) {
const identifier = setEmitFlags(factory.createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping);
identifier.symbol = symbol;
@ -23533,35 +23531,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* @param text a valid bigint string excluding a trailing `n`, but including a possible prefix `-`. Use `isValidBigIntString(text, roundTripOnly)` before calling this function.
*/
function parseBigIntLiteralType(text: string) {
const negative = text.startsWith("-");
const base10Value = parsePseudoBigInt(`${negative ? text.slice(1) : text}n`);
return getBigIntLiteralType({ negative, base10Value });
}
/**
* Tests whether the provided string can be parsed as a bigint.
* @param s The string to test.
* @param roundTripOnly Indicates the resulting bigint matches the input when converted back to a string.
*/
function isValidBigIntString(s: string, roundTripOnly: boolean): boolean {
if (s === "") return false;
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
let success = true;
scanner.setOnError(() => success = false);
scanner.setText(s + "n");
let result = scanner.scan();
const negative = result === SyntaxKind.MinusToken;
if (negative) {
result = scanner.scan();
}
const flags = scanner.getTokenFlags();
// validate that
// * scanning proceeded without error
// * a bigint can be scanned, and that when it is scanned, it is
// * the full length of the input string (so the scanner is one character beyond the augmented input length)
// * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input)
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator)
&& (!roundTripOnly || s === pseudoBigIntToString({ negative, base10Value: parsePseudoBigInt(scanner.getTokenValue()) }));
return getBigIntLiteralType(parseValidBigInt(text));
}
function isMemberOfStringMapping(source: Type, target: Type): boolean {

View File

@ -187,13 +187,14 @@ import {
getResolutionMode,
getResolutionName,
getRootLength,
getSnippetElement,
getStringComparer,
getSymbolId,
getTrailingCommentRanges,
HasExpressionInitializer,
hasExtension,
hasInitializer,
HasInitializer,
hasInitializer,
HasJSDoc,
hasJSDocNodes,
HasModifiers,
@ -257,6 +258,7 @@ import {
isGetAccessorDeclaration,
isHeritageClause,
isIdentifier,
isIdentifierStart,
isIdentifierText,
isImportTypeNode,
isInterfaceDeclaration,
@ -440,6 +442,7 @@ import {
singleOrUndefined,
skipOuterExpressions,
skipTrivia,
SnippetKind,
some,
sort,
SortedArray,
@ -8555,6 +8558,51 @@ export function pseudoBigIntToString({negative, base10Value}: PseudoBigInt): str
return (negative && base10Value !== "0" ? "-" : "") + base10Value;
}
/** @internal */
export function parseBigInt(text: string): PseudoBigInt | undefined {
if (!isValidBigIntString(text, /*roundTripOnly*/ false)) {
return undefined;
}
return parseValidBigInt(text);
}
/**
* @internal
* @param text a valid bigint string excluding a trailing `n`, but including a possible prefix `-`. Use `isValidBigIntString(text, roundTripOnly)` before calling this function.
*/
export function parseValidBigInt(text: string): PseudoBigInt {
const negative = text.startsWith("-");
const base10Value = parsePseudoBigInt(`${negative ? text.slice(1) : text}n`);
return { negative, base10Value };
}
/**
* @internal
* Tests whether the provided string can be parsed as a bigint.
* @param s The string to test.
* @param roundTripOnly Indicates the resulting bigint matches the input when converted back to a string.
*/
export function isValidBigIntString(s: string, roundTripOnly: boolean): boolean {
if (s === "") return false;
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
let success = true;
scanner.setOnError(() => success = false);
scanner.setText(s + "n");
let result = scanner.scan();
const negative = result === SyntaxKind.MinusToken;
if (negative) {
result = scanner.scan();
}
const flags = scanner.getTokenFlags();
// validate that
// * scanning proceeded without error
// * a bigint can be scanned, and that when it is scanned, it is
// * the full length of the input string (so the scanner is one character beyond the augmented input length)
// * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input)
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator)
&& (!roundTripOnly || s === pseudoBigIntToString({ negative, base10Value: parsePseudoBigInt(scanner.getTokenValue()) }));
}
/** @internal */
export function isValidTypeOnlyAliasUseSite(useSite: Node): boolean {
return !!(useSite.flags & NodeFlags.Ambient)
@ -9062,4 +9110,21 @@ export function isOptionalJSDocPropertyLikeTag(node: Node): node is JSDocPropert
}
const { isBracketed, typeExpression } = node;
return isBracketed || !!typeExpression && typeExpression.type.kind === SyntaxKind.JSDocOptionalType;
}
/** @internal */
export function canUsePropertyAccess(name: string, languageVersion: ScriptTarget): boolean {
if (name.length === 0) {
return false;
}
const firstChar = name.charCodeAt(0);
return firstChar === CharacterCodes.hash ?
name.length > 1 && isIdentifierStart(name.charCodeAt(1), languageVersion) :
isIdentifierStart(firstChar, languageVersion);
}
/** @internal */
export function hasTabstop(node: Node): boolean {
return getSnippetElement(node)?.kind === SnippetKind.TabStop;
}

View File

@ -5,6 +5,9 @@ import {
BinaryExpression,
BreakOrContinueStatement,
CancellationToken,
canUsePropertyAccess,
CaseBlock,
CaseClause,
cast,
CharacterCodes,
ClassElement,
@ -40,11 +43,15 @@ import {
createTextSpanFromRange,
Debug,
Declaration,
DefaultClause,
Diagnostics,
diagnosticToString,
displayPart,
EmitHint,
EmitTextWriter,
endsWith,
EntityName,
EnumMember,
escapeSnippetText,
every,
ExportKind,
@ -106,6 +113,7 @@ import {
ImportSpecifier,
ImportTypeNode,
IncompleteCompletionsCache,
IndexedAccessTypeNode,
insertSorted,
InternalSymbolName,
isAbstractConstructorSymbol,
@ -117,6 +125,7 @@ import {
isBindingPattern,
isBreakOrContinueStatement,
isCallExpression,
isCaseBlock,
isCaseClause,
isCheckJsEnabledForFile,
isClassElement,
@ -128,8 +137,10 @@ import {
isConstructorDeclaration,
isContextualKeyword,
isDeclarationName,
isDefaultClause,
isDeprecatedDeclaration,
isEntityName,
isEnumMember,
isEqualityOperatorKind,
isExportAssignment,
isExportDeclaration,
@ -169,6 +180,7 @@ import {
isKeyword,
isKnownSymbol,
isLabeledStatement,
isLiteralExpression,
isLiteralImportTypeNode,
isMemberName,
isMethodDeclaration,
@ -238,6 +250,9 @@ import {
lastOrUndefined,
length,
ListFormat,
LiteralType,
LiteralTypeNode,
map,
mapDefined,
maybeBind,
MemberOverrideStatus,
@ -257,11 +272,14 @@ import {
NodeBuilderFlags,
NodeFlags,
nodeIsMissing,
NumericLiteral,
ObjectBindingPattern,
ObjectLiteralExpression,
ObjectType,
ObjectTypeDeclaration,
or,
ParenthesizedTypeNode,
parseBigInt,
positionBelongsToNode,
positionIsASICandidate,
positionsAreOnSameLine,
@ -324,7 +342,10 @@ import {
TypeFlags,
typeHasCallOrConstructSignatures,
TypeLiteralNode,
TypeNode,
TypeOnlyAliasDeclaration,
TypeQueryNode,
TypeReferenceNode,
unescapeLeadingUnderscores,
UnionReduction,
UnionType,
@ -394,6 +415,8 @@ export enum CompletionSource {
TypeOnlyAlias = "TypeOnlyAlias/",
/** Auto-import that comes attached to an object literal method snippet */
ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/",
/** Case completions for switch statements */
SwitchCases = "SwitchCases/",
}
/** @internal */
@ -915,6 +938,16 @@ function completionInfoFromData(
getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries);
}
let caseBlock: CaseBlock | undefined;
if (preferences.includeCompletionsWithInsertText
&& contextToken
&& (caseBlock = findAncestor(contextToken, isCaseBlock))) {
const cases = getExhaustiveCaseSnippets(caseBlock, sourceFile, preferences, compilerOptions, host, program, formatContext);
if (cases) {
entries.push(cases.entry);
}
}
return {
flags: completionData.flags,
isGlobalCompletion: isInSnippetScope,
@ -930,6 +963,224 @@ function isCheckedFile(sourceFile: SourceFile, compilerOptions: CompilerOptions)
return !isSourceFileJS(sourceFile) || !!isCheckJsEnabledForFile(sourceFile, compilerOptions);
}
function getExhaustiveCaseSnippets(
caseBlock: CaseBlock,
sourceFile: SourceFile,
preferences: UserPreferences,
options: CompilerOptions,
host: LanguageServiceHost,
program: Program,
formatContext: formatting.FormatContext | undefined): { entry: CompletionEntry, importAdder: codefix.ImportAdder } | undefined {
const clauses = caseBlock.clauses;
const checker = program.getTypeChecker();
const switchType = checker.getTypeAtLocation(caseBlock.parent.expression);
if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) {
// Collect constant values in existing clauses.
const tracker = newCaseClauseTracker(checker, clauses);
const target = getEmitScriptTarget(options);
const quotePreference = getQuotePreference(sourceFile, preferences);
const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host);
const elements: Expression[] = [];
for (const type of switchType.types as LiteralType[]) {
// Enums
if (type.flags & TypeFlags.EnumLiteral) {
Debug.assert(type.symbol, "An enum member type should have a symbol");
Debug.assert(type.symbol.parent, "An enum member type should have a parent symbol (the enum symbol)");
// Filter existing enums by their values
const enumValue = type.symbol.valueDeclaration && checker.getConstantValue(type.symbol.valueDeclaration as EnumMember);
if (enumValue !== undefined) {
if (tracker.hasValue(enumValue)) {
continue;
}
tracker.addValue(enumValue);
}
const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseBlock, target);
if (!typeNode) {
return undefined;
}
const expr = typeNodeToExpression(typeNode, target, quotePreference);
if (!expr) {
return undefined;
}
elements.push(expr);
}
// Literals
else if (!tracker.hasValue(type.value)) {
switch (typeof type.value) {
case "object":
elements.push(factory.createBigIntLiteral(type.value));
break;
case "number":
elements.push(factory.createNumericLiteral(type.value));
break;
case "string":
elements.push(factory.createStringLiteral(type.value));
break;
}
}
}
if (elements.length === 0) {
return undefined;
}
const newClauses = map(elements, element => factory.createCaseClause(element, []));
const newLineChar = getNewLineCharacter(options, maybeBind(host, host.getNewLine));
const printer = createSnippetPrinter({
removeComments: true,
module: options.module,
target: options.target,
newLine: getNewLineKind(newLineChar),
});
const printNode = formatContext
? (node: Node) => printer.printAndFormatNode(EmitHint.Unspecified, node, sourceFile, formatContext)
: (node: Node) => printer.printNode(EmitHint.Unspecified, node, sourceFile);
const insertText = map(newClauses, (clause, i) => {
if (preferences.includeCompletionsWithSnippetText) {
return `${printNode(clause)}$${i+1}`;
}
return `${printNode(clause)}`;
}).join(newLineChar);
const firstClause = printer.printNode(EmitHint.Unspecified, newClauses[0], sourceFile);
return {
entry: {
name: `${firstClause} ...`,
kind: ScriptElementKind.unknown,
sortText: SortText.GlobalsOrKeywords,
insertText,
hasAction: importAdder.hasFixes() || undefined,
source: CompletionSource.SwitchCases,
isSnippet: preferences.includeCompletionsWithSnippetText ? true : undefined,
},
importAdder,
};
}
return undefined;
}
interface CaseClauseTracker {
addValue(value: string | number): void;
hasValue(value: string | number | PseudoBigInt): boolean;
}
function newCaseClauseTracker(checker: TypeChecker, clauses: readonly (CaseClause | DefaultClause)[]): CaseClauseTracker {
const existingStrings = new Set<string>();
const existingNumbers = new Set<number>();
const existingBigInts = new Set<string>();
for (const clause of clauses) {
if (!isDefaultClause(clause)) {
if (isLiteralExpression(clause.expression)) {
const expression = clause.expression;
switch (expression.kind) {
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.StringLiteral:
existingStrings.add(expression.text);
break;
case SyntaxKind.NumericLiteral:
existingNumbers.add(parseInt(expression.text));
break;
case SyntaxKind.BigIntLiteral:
const parsedBigInt = parseBigInt(endsWith(expression.text, "n") ? expression.text.slice(0, -1) : expression.text);
if (parsedBigInt) {
existingBigInts.add(pseudoBigIntToString(parsedBigInt));
}
break;
}
}
else {
const symbol = checker.getSymbolAtLocation(clause.expression);
if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) {
const enumValue = checker.getConstantValue(symbol.valueDeclaration);
if (enumValue !== undefined) {
addValue(enumValue);
}
}
}
}
}
return {
addValue,
hasValue,
};
function addValue(value: string | number) {
switch (typeof value) {
case "string":
existingStrings.add(value);
break;
case "number":
existingNumbers.add(value);
}
}
function hasValue(value: string | number | PseudoBigInt): boolean {
switch (typeof value) {
case "string":
return existingStrings.has(value);
case "number":
return existingNumbers.has(value);
case "object":
return existingBigInts.has(pseudoBigIntToString(value));
}
}
}
function typeNodeToExpression(typeNode: TypeNode, languageVersion: ScriptTarget, quotePreference: QuotePreference): Expression | undefined {
switch (typeNode.kind) {
case SyntaxKind.TypeReference:
const typeName = (typeNode as TypeReferenceNode).typeName;
return entityNameToExpression(typeName, languageVersion, quotePreference);
case SyntaxKind.IndexedAccessType:
const objectExpression =
typeNodeToExpression((typeNode as IndexedAccessTypeNode).objectType, languageVersion, quotePreference);
const indexExpression =
typeNodeToExpression((typeNode as IndexedAccessTypeNode).indexType, languageVersion, quotePreference);
return objectExpression
&& indexExpression
&& factory.createElementAccessExpression(objectExpression, indexExpression);
case SyntaxKind.LiteralType:
const literal = (typeNode as LiteralTypeNode).literal;
switch (literal.kind) {
case SyntaxKind.StringLiteral:
return factory.createStringLiteral(literal.text, quotePreference === QuotePreference.Single);
case SyntaxKind.NumericLiteral:
return factory.createNumericLiteral(literal.text, (literal as NumericLiteral).numericLiteralFlags);
}
return undefined;
case SyntaxKind.ParenthesizedType:
const exp = typeNodeToExpression((typeNode as ParenthesizedTypeNode).type, languageVersion, quotePreference);
return exp && (isIdentifier(exp) ? exp : factory.createParenthesizedExpression(exp));
case SyntaxKind.TypeQuery:
return entityNameToExpression((typeNode as TypeQueryNode).exprName, languageVersion, quotePreference);
case SyntaxKind.ImportType:
Debug.fail(`We should not get an import type after calling 'codefix.typeToAutoImportableTypeNode'.`);
}
return undefined;
}
function entityNameToExpression(entityName: EntityName, languageVersion: ScriptTarget, quotePreference: QuotePreference): Expression {
if (isIdentifier(entityName)) {
return entityName;
}
const unescapedName = unescapeLeadingUnderscores(entityName.right.escapedText);
if (canUsePropertyAccess(unescapedName, languageVersion)) {
return factory.createPropertyAccessExpression(
entityNameToExpression(entityName.left, languageVersion, quotePreference),
unescapedName);
}
else {
return factory.createElementAccessExpression(
entityNameToExpression(entityName.left, languageVersion, quotePreference),
factory.createStringLiteral(unescapedName, quotePreference === QuotePreference.Single));
}
}
function isMemberCompletionKind(kind: CompletionKind): boolean {
switch (kind) {
case CompletionKind.ObjectPropertyDeclaration:
@ -1563,6 +1814,8 @@ function createSnippetPrinter(
return {
printSnippetList,
printAndFormatSnippetList,
printNode,
printAndFormatNode,
};
// The formatter/scanner will have issues with snippet-escaped text,
@ -1582,7 +1835,7 @@ function createSnippetPrinter(
}
}
/* Snippet-escaping version of `printer.printList`. */
/** Snippet-escaping version of `printer.printList`. */
function printSnippetList(
format: ListFormat,
list: NodeArray<Node>,
@ -1636,6 +1889,50 @@ function createSnippetPrinter(
: changes;
return textChanges.applyChanges(syntheticFile.text, allChanges);
}
/** Snippet-escaping version of `printer.printNode`. */
function printNode(hint: EmitHint, node: Node, sourceFile: SourceFile): string {
const unescaped = printUnescapedNode(hint, node, sourceFile);
return escapes ? textChanges.applyChanges(unescaped, escapes) : unescaped;
}
function printUnescapedNode(hint: EmitHint, node: Node, sourceFile: SourceFile): string {
escapes = undefined;
writer.clear();
printer.writeNode(hint, node, sourceFile, writer);
return writer.getText();
}
function printAndFormatNode(
hint: EmitHint,
node: Node,
sourceFile: SourceFile,
formatContext: formatting.FormatContext): string {
const syntheticFile = {
text: printUnescapedNode(
hint,
node,
sourceFile),
getLineAndCharacterOfPosition(pos: number) {
return getLineAndCharacterOfPosition(this, pos);
},
};
const formatOptions = getFormatCodeSettingsForWriting(formatContext, sourceFile);
const nodeWithPos = textChanges.assignPositionsToNode(node);
const changes = formatting.formatNodeGivenIndentation(
nodeWithPos,
syntheticFile,
sourceFile.languageVariant,
/* indentation */ 0,
/* delta */ 0,
{ ...formatContext, options: formatOptions });
const allChanges = escapes
? stableSort(concatenate(changes, escapes), (a, b) => compareTextSpans(a.span, b.span))
: changes;
return textChanges.applyChanges(syntheticFile.text, allChanges);
}
}
function originToCompletionEntryData(origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport): CompletionEntryData | undefined {
@ -1928,7 +2225,10 @@ function getSymbolCompletionFromEntryId(
entryId: CompletionEntryIdentifier,
host: LanguageServiceHost,
preferences: UserPreferences,
): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number | PseudoBigInt } | { type: "none" } {
): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number | PseudoBigInt } | { type: "cases" } | { type: "none" } {
if (entryId.source === CompletionSource.SwitchCases) {
return { type: "cases" };
}
if (entryId.data) {
const autoImport = getAutoImportSymbolFromCompletionEntryData(entryId.name, entryId.data, program, host);
if (autoImport) {
@ -1999,9 +2299,9 @@ export function getCompletionEntryDetails(
const compilerOptions = program.getCompilerOptions();
const { name, source, data } = entryId;
const contextToken = findPrecedingToken(position, sourceFile);
if (isInString(sourceFile, position, contextToken)) {
return StringCompletions.getStringLiteralCompletionDetails(name, sourceFile, position, contextToken, typeChecker, compilerOptions, host, cancellationToken, preferences);
const { previousToken, contextToken } = getRelevantTokens(position, sourceFile);
if (isInString(sourceFile, position, previousToken)) {
return StringCompletions.getStringLiteralCompletionDetails(name, sourceFile, position, previousToken, typeChecker, compilerOptions, host, cancellationToken, preferences);
}
// Compute all the completion symbols again.
@ -2031,6 +2331,39 @@ export function getCompletionEntryDetails(
const { literal } = symbolCompletion;
return createSimpleDetails(completionNameForLiteral(sourceFile, preferences, literal), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral);
}
case "cases": {
const { entry, importAdder } = getExhaustiveCaseSnippets(
contextToken!.parent as CaseBlock,
sourceFile,
preferences,
program.getCompilerOptions(),
host,
program,
/*formatContext*/ undefined)!;
if (importAdder.hasFixes()) {
const changes = textChanges.ChangeTracker.with(
{ host, formatContext, preferences },
importAdder.writeFixes);
return {
name: entry.name,
kind: ScriptElementKind.unknown,
kindModifiers: "",
displayParts: [],
sourceDisplay: undefined,
codeActions: [{
changes,
description: diagnosticToString([Diagnostics.Includes_imports_of_types_referenced_by_0, name]),
}],
};
}
return {
name: entry.name,
kind: ScriptElementKind.unknown,
kindModifiers: "",
displayParts: [],
sourceDisplay: undefined,
};
}
case "none":
// Didn't find a symbol with this name. See if we can find a keyword instead.
return allKeywordsCompletions().some(c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined;

View File

@ -122,6 +122,7 @@ import {
hasProperty,
hasStaticModifier,
hasSyntacticModifier,
hasTabstop,
HighlightSpanKind,
HostCancellationToken,
hostGetCanonicalFileName,
@ -182,8 +183,8 @@ import {
isTagName,
isTextWhiteSpaceLike,
isThisTypeParameter,
JsDoc,
JSDoc,
JsDoc,
JSDocContainer,
JSDocTagInfo,
JsonSourceFile,
@ -497,6 +498,9 @@ function addSyntheticNodes(nodes: Push<Node>, pos: number, end: number, parent:
const textPos = scanner.getTextPos();
if (textPos <= end) {
if (token === SyntaxKind.Identifier) {
if (hasTabstop(parent)) {
continue;
}
Debug.fail(`Did not expect ${Debug.formatSyntaxKind(parent.kind)} to have an Identifier in its trivia`);
}
nodes.push(createNode(token, pos, textPos, parent));

View File

@ -0,0 +1,107 @@
/// <reference path="fourslash.ts" />
// Basic tests
// @newline: LF
//// enum E {
//// A = 0,
//// B = "B",
//// C = "C",
//// }
//// // Mixed union
//// declare const u: E.A | E.B | 1;
//// switch (u) {
//// case/*1*/
//// }
//// // Union enum
//// declare const e: E;
//// switch (e) {
//// case/*2*/
//// }
//// enum F {
//// D = 1 << 0,
//// E = 1 << 1,
//// F = 1 << 2,
//// }
////
//// declare const f: F;
//// switch (f) {
//// case/*3*/
//// }
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
{
name: "case E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.A:
case E.B:
case 1:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "2",
isNewIdentifierLocation: false,
includes: [
{
name: "case E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.A:
case E.B:
case E.C:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "3",
isNewIdentifierLocation: false,
includes: [
{
name: "case F.D: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case F.D:
case F.E:
case F.F:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "3",
isNewIdentifierLocation: false,
includes: [
{
name: "case F.D: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
isSnippet: true,
insertText:
`case F.D:$1
case F.E:$2
case F.F:$3`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
includeCompletionsWithSnippetText: true,
},
},
);

View File

@ -0,0 +1,77 @@
/// <reference path="fourslash.ts" />
// Import-related cases
// @newline: LF
// @Filename: /dep.ts
//// export enum E {
//// A = 0,
//// B = "B",
//// C = "C",
//// }
//// declare const u: E.A | E.B | 1;
//// export { u };
// @Filename: /main.ts
//// import { u } from "./dep";
//// switch (u) {
//// case/*1*/
//// }
// @Filename: /other.ts
//// import * as d from "./dep";
//// declare const u: d.E;
//// switch (u) {
//// case/*2*/
//// }
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
{
name: "case E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.A:
case E.B:
case 1:`,
hasAction: true,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "2",
isNewIdentifierLocation: false,
includes: [
{
name: "case d.E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case d.E.A:
case d.E.B:
case d.E.C:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
);
verify.applyCodeActionFromCompletion("1", {
name: "case E.A: ...",
source: "SwitchCases/",
description: "Includes imports of types referenced by 'case E.A: ...'",
newFileContent:
`import { E, u } from "./dep";
switch (u) {
case
}`,
});

View File

@ -0,0 +1,112 @@
/// <reference path="fourslash.ts" />
// Where the exhaustive case completion appears or not.
// @newline: LF
// @Filename: /main.ts
//// enum E {
//// A = 0,
//// B = "B",
//// C = "C",
//// }
//// declare const u: E;
//// switch (u) {
//// case/*1*/
//// }
//// switch (u) {
//// /*2*/
//// }
//// switch (u) {
//// case 1:
//// /*3*/
//// }
//// switch (u) {
//// c/*4*/
//// }
//// switch (u) {
//// case /*5*/
//// }
//// /*6*/
//// switch (u) {
//// /*7*/
////
const exhaustiveCaseCompletion = {
name: "case E.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.A:
case E.B:
case E.C:`,
};
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "2",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "3",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "4",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "5",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "6",
exact: [
"E",
"u",
...completion.globals,
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
{
marker: "7",
includes: [
exhaustiveCaseCompletion,
],
preferences: {
includeCompletionsWithInsertText: true,
}
},
);

View File

@ -0,0 +1,179 @@
/// <reference path="fourslash.ts" />
// Filter existing values.
// @newline: LF
//// enum E {
//// A = 0,
//// B = "B",
//// C = "C",
//// }
//// // Filtering existing literals
//// declare const u: E.A | E.B | 1 | 1n | "1";
//// switch (u) {
//// case E.A:
//// case 1:
//// case 1n:
//// case 0x1n:
//// case "1":
//// case `1`:
//// case `1${u}`:
//// case/*1*/
//// }
//// declare const v: E.A | "1" | "2";
//// switch (v) {
//// case 0:
//// case `1`:
//// /*2*/
//// }
//// // Filtering repreated enum members
//// enum F {
//// A = "A",
//// B = "B",
//// C = A,
//// }
//// declare const x: F;
//// switch (x) {
//// /*3*/
//// }
//// // Enum with computed elements
//// enum G {
//// C = 0,
//// D = 1 << 1,
//// E = 1 << 2,
//// OtherD = D,
//// DorE = D | E,
//// }
//// declare const y: G;
//// switch (y) {
//// /*4*/
//// }
//// switch (y) {
//// case 0: // same as G.C
//// case 1: // same as G.D, but we don't know it
//// case 3: // same as G.DorE, but we don't know
//// /*5*/
//// }
////
//// // Already exhaustive switch
//// enum H {
//// A = "A",
//// B = "B",
//// C = "C",
//// }
//// declare const z: H;
//// switch (z) {
//// case H.A:
//// case H.B:
//// case H.C:
//// /*6*/
//// }
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
{
name: "case E.B: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case E.B:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "2",
isNewIdentifierLocation: false,
includes: [
{
name: `case "2": ...`,
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case "2":`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "3",
isNewIdentifierLocation: false,
includes: [
{
name: "case F.A: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case F.A:
case F.B:`, // no C because C's value is the same as A's
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "4",
isNewIdentifierLocation: false,
includes: [
{
name: "case G.C: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case G.C:
case G.D:
case G.E:
case G.DorE:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "5",
isNewIdentifierLocation: false,
includes: [
{
name: "case G.D: ...",
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case G.D:
case G.E:
case G.DorE:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
{
marker: "6",
isNewIdentifierLocation: false,
// No exhaustive case completion offered here because the switch is already exhaustive
exact: [
"E",
"F",
"G",
"H",
"u",
"v",
"x",
"y",
"z",
...completion.globals,
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
);

View File

@ -0,0 +1,35 @@
/// <reference path="fourslash.ts" />
// Filter existing values.
// @newline: LF
//// enum P {
//// " Space",
//// Bar,
//// }
////
//// declare const p: P;
////
//// switch (p) {
//// /*1*/
//// }
verify.completions(
{
marker: "1",
isNewIdentifierLocation: false,
includes: [
{
name: `case P[" Space"]: ...`,
source: completion.CompletionSource.SwitchCases,
sortText: completion.SortText.GlobalsOrKeywords,
insertText:
`case P[" Space"]:
case P.Bar:`,
},
],
preferences: {
includeCompletionsWithInsertText: true,
},
},
);

View File

@ -884,6 +884,7 @@ declare namespace completion {
ClassMemberSnippet = "ClassMemberSnippet/",
TypeOnlyAlias = "TypeOnlyAlias/",
ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/",
SwitchCases = "SwitchCases/",
}
export const globalThisEntry: Entry;
export const undefinedVarEntry: Entry;