diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e9241cb0b7457..8617a965feccd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -52,6 +52,7 @@ import { canHaveIllegalDecorators, canHaveIllegalModifiers, canHaveModifiers, + canUsePropertyAccess, cartesianProduct, CaseBlock, CaseClause, @@ -101,7 +102,6 @@ import { createGetSymbolWalker, createPrinter, createPropertyNameNodeForIdentifierOrLiteral, - createScanner, createSymbolTable, createTextWriter, createUnderscoreEscapedMultiMap, @@ -337,8 +337,8 @@ import { hasAccessorModifier, hasAmbientModifier, hasContextSensitiveParameters, - hasDecorators, HasDecorators, + hasDecorators, hasDynamicName, hasEffectiveModifier, hasEffectiveModifiers, @@ -347,8 +347,8 @@ import { hasExtension, HasIllegalDecorators, HasIllegalModifiers, - hasInitializer, HasInitializer, + hasInitializer, hasJSDocNodes, hasJSDocParameterTags, hasJsonModuleEmitEnabled, @@ -499,7 +499,6 @@ import { isGlobalScopeAugmentation, isHeritageClause, isIdentifier, - isIdentifierStart, isIdentifierText, isIdentifierTypePredicate, isIdentifierTypeReference, @@ -677,6 +676,7 @@ import { isTypeReferenceNode, isTypeReferenceType, isUMDExportSymbol, + isValidBigIntString, isValidESSymbolDeclaration, isValidTypeOnlyAliasUseSite, isValueSignatureDeclaration, @@ -826,6 +826,7 @@ import { parseIsolatedEntityName, parseNodeFactory, parsePseudoBigInt, + parseValidBigInt, Path, pathIsRelative, PatternAmbientModule, @@ -7694,10 +7695,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; @@ -23536,35 +23534,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 { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 8de47c348430e..36cfe74f91e01 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -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; } diff --git a/src/services/completions.ts b/src/services/completions.ts index 11afb4c8c0ab9..a09aebcb7d1a3 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -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(); + const existingNumbers = new Set(); + const existingBigInts = new Set(); + + 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, @@ -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; diff --git a/src/services/services.ts b/src/services/services.ts index 41b3c78172ad1..374d59223a2ff 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -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, 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)); diff --git a/tests/cases/fourslash/exhaustiveCaseCompletions1.ts b/tests/cases/fourslash/exhaustiveCaseCompletions1.ts new file mode 100644 index 0000000000000..a0bef292f1b34 --- /dev/null +++ b/tests/cases/fourslash/exhaustiveCaseCompletions1.ts @@ -0,0 +1,107 @@ +/// + +// 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, + }, + }, +); \ No newline at end of file diff --git a/tests/cases/fourslash/exhaustiveCaseCompletions2.ts b/tests/cases/fourslash/exhaustiveCaseCompletions2.ts new file mode 100644 index 0000000000000..777cf484b6abc --- /dev/null +++ b/tests/cases/fourslash/exhaustiveCaseCompletions2.ts @@ -0,0 +1,77 @@ +/// + +// 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 +}`, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/exhaustiveCaseCompletions3.ts b/tests/cases/fourslash/exhaustiveCaseCompletions3.ts new file mode 100644 index 0000000000000..248acab7e1742 --- /dev/null +++ b/tests/cases/fourslash/exhaustiveCaseCompletions3.ts @@ -0,0 +1,112 @@ +/// + +// 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, + } + }, +); \ No newline at end of file diff --git a/tests/cases/fourslash/exhaustiveCaseCompletions4.ts b/tests/cases/fourslash/exhaustiveCaseCompletions4.ts new file mode 100644 index 0000000000000..6240b4454f082 --- /dev/null +++ b/tests/cases/fourslash/exhaustiveCaseCompletions4.ts @@ -0,0 +1,179 @@ +/// + +// 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, + }, + }, +); \ No newline at end of file diff --git a/tests/cases/fourslash/exhaustiveCaseCompletions5.ts b/tests/cases/fourslash/exhaustiveCaseCompletions5.ts new file mode 100644 index 0000000000000..eee046c77cf5e --- /dev/null +++ b/tests/cases/fourslash/exhaustiveCaseCompletions5.ts @@ -0,0 +1,35 @@ +/// + +// 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, + }, + }, +); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index d8d17f21af973..7d106808e7982 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -884,6 +884,7 @@ declare namespace completion { ClassMemberSnippet = "ClassMemberSnippet/", TypeOnlyAlias = "TypeOnlyAlias/", ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/", + SwitchCases = "SwitchCases/", } export const globalThisEntry: Entry; export const undefinedVarEntry: Entry;