From f6c3566695b72ea25d320c2ee020c169a2729778 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 23 Sep 2022 15:48:15 -0700 Subject: [PATCH 01/23] fix services' type's isLiteral --- src/services/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/services.ts b/src/services/services.ts index 975e36b1dd40c..080711bff5781 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -511,7 +511,7 @@ namespace ts { return !!(this.flags & TypeFlags.UnionOrIntersection); } isLiteral(): this is LiteralType { - return !!(this.flags & TypeFlags.StringOrNumberLiteral); + return !!(this.flags & TypeFlags.Literal); } isStringLiteral(): this is StringLiteralType { return !!(this.flags & TypeFlags.StringLiteral); From 4467330dd1c6bf99f9e4ae767eb8d0f0734b2043 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 23 Sep 2022 16:13:24 -0700 Subject: [PATCH 02/23] update literal completions tests --- tests/cases/fourslash/completionsLiterals.ts | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/cases/fourslash/completionsLiterals.ts b/tests/cases/fourslash/completionsLiterals.ts index 475d3dd247edf..639a837907567 100644 --- a/tests/cases/fourslash/completionsLiterals.ts +++ b/tests/cases/fourslash/completionsLiterals.ts @@ -1,6 +1,8 @@ /// ////const x: 0 | "one" = /**/; +////const y: 0 | "one" | 1n | true = /*1*/; +////const z: boolean = /*2*/; verify.completions({ marker: "", @@ -10,3 +12,22 @@ verify.completions({ ], isNewIdentifierLocation: true, }); +verify.completions({ + marker: "1", + includes: [ + { name: "0", kind: "string", text: "0" }, + { name: '"one"', kind: "string", text: '"one"' }, + { name: "1n", kind: "string", text: "1n" }, + { name: "true", kind: "keyword", text: "true", sortText: completion.SortText.GlobalsOrKeywords }, + ], + isNewIdentifierLocation: true, +}); + +verify.completions({ + marker: "2", + includes: [ + { name: "true", kind: "keyword", text: "true", sortText: completion.SortText.GlobalsOrKeywords }, + { name: "false", kind: "keyword", text: "false", sortText: completion.SortText.GlobalsOrKeywords }, + ], + isNewIdentifierLocation: true, +}); From a206fe1407d4bb2c9dda2a7b7b5cdae7df3fa584 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 7 Sep 2022 16:50:33 -0700 Subject: [PATCH 03/23] initial prototype --- src/compiler/factory/nodeTests.ts | 5 + src/compiler/types.ts | 1 + src/services/completions.ts | 107 +++++++++++++++++- .../cases/fourslash/contextualTypeInSwitch.ts | 80 +++++++++++++ 4 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 tests/cases/fourslash/contextualTypeInSwitch.ts diff --git a/src/compiler/factory/nodeTests.ts b/src/compiler/factory/nodeTests.ts index a2a558e9079b1..8a288b6957454 100644 --- a/src/compiler/factory/nodeTests.ts +++ b/src/compiler/factory/nodeTests.ts @@ -154,6 +154,11 @@ namespace ts { 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 66e55ab58b381..ba4489ef545da 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1326,6 +1326,7 @@ namespace ts { export type AssertsKeyword = KeywordToken; 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 bff90e2d3e7c5..bfde032dea331 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -264,12 +264,13 @@ namespace ts.Completions { } if (triggerCharacter === " ") { - // `isValidTrigger` ensures we are at `import |` - if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { - return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; + // `isValidTrigger` ensures we are at `import |` or `case |`. + if (previousToken && isImportKeyword(previousToken)) { + if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { + return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; + } + return undefined; } - return undefined; - } // If the request is a continuation of an earlier `isIncomplete` response, @@ -564,6 +565,14 @@ namespace ts.Completions { getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries); } + // >> TODO: prototype remaining cases here + if (contextToken && isCaseKeyword(contextToken) && preferences.includeCompletionsWithInsertText) { + const casesEntry = getExhaustiveCaseSnippets(contextToken, sourceFile, preferences, compilerOptions, host, program.getTypeChecker(), formatContext); + if (casesEntry) { + entries.push(casesEntry); + } + } + return { flags: completionData.flags, isGlobalCompletion: isInSnippetScope, @@ -579,6 +588,90 @@ namespace ts.Completions { return !isSourceFileJS(sourceFile) || !!isCheckJsEnabledForFile(sourceFile, compilerOptions); } + function getExhaustiveCaseSnippets( + contextToken: Token, + sourceFile: SourceFile, + preferences: UserPreferences, + options: CompilerOptions, + host: LanguageServiceHost, + checker: TypeChecker, + formatContext: formatting.FormatContext | undefined): CompletionEntry | undefined { + const caseClause = tryCast(contextToken.parent, isCaseClause); + if (caseClause) { + const switchType = getSwitchedType(caseClause, checker); + if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) { // >> TODO: does this work for enum members? + const elements: [string, Expression][] = mapDefined(switchType.types as LiteralType[], type => { + if (type.flags & TypeFlags.EnumLiteral) { + Debug.assert(type.symbol, "TODO: should this hold always?"); + Debug.assert(type.symbol.parent, "TODO: should this hold always too?"); + const target = getEmitScriptTarget(options); + // >> TODO: figure out if need an import action + const memberInfo = getCompletionEntryDisplayNameForSymbol( + type.symbol, + target, + /*origin*/ undefined, + CompletionKind.None, /*jsxIdentifierExpected*/ false); + const enumInfo = getCompletionEntryDisplayNameForSymbol( + type.symbol.parent, + target, + /*origin*/ undefined, + CompletionKind.None, /*jsxIdentifierExpected*/ false); + if (memberInfo && enumInfo) { + const enumExp = factory.createIdentifier(enumInfo.name); // >> TODO: have to properly get the exp + const access: Expression = factory.createPropertyAccessExpression(enumExp, memberInfo.name); // >> TODO: we might need to use [] to access this member + // >> TODO: convert access to text properly + return [`${enumInfo.name}.${memberInfo.name}`, access]; // >> TODO: this looks hacky, do it some other way + } + } + else { + const text = completionNameForLiteral(sourceFile, preferences, type.value); + const literal: Expression = typeof type.value === "object" + ? factory.createBigIntLiteral(type.value) + : typeof type.value === "number" + ? factory.createNumericLiteral(type.value) + : factory.createStringLiteral(type.value); + return [text, literal]; + } + return undefined; // >> TODO: actually only do this if every return is defined, + // >> otherwise won't really be exhaustive + }); + + const clauses = map(elements, element => { + return factory.createCaseClause(element[1], []); + }); + const printer = createSnippetPrinter({ + removeComments: true, + module: options.module, + target: options.target, + newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), + }); + // >> TODO: get format context and format before printing, if possible + const insertText = formatContext + ? printer.printAndFormatSnippetList( + ListFormat.MultiLine | ListFormat.NoTrailingNewLine, + factory.createNodeArray(clauses), + sourceFile, + formatContext) + : printer.printSnippetList( + ListFormat.MultiLine | ListFormat.NoTrailingNewLine, + factory.createNodeArray(clauses), + sourceFile); + + return { + name: elements[0][0], + isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one + kind: ScriptElementKind.unknown, // >> TODO: what should this be? + sortText: SortText.LocalDeclarationPriority, + insertText, + replacementSpan: getReplacementSpanForContextToken(contextToken), + } + // >> TODO: filter cases that are already there + } + } + + return undefined; + } + function isMemberCompletionKind(kind: CompletionKind): boolean { switch (kind) { case CompletionKind.ObjectPropertyDeclaration: @@ -4191,7 +4284,9 @@ namespace ts.Completions { ? !!tryGetImportFromModuleSpecifier(contextToken) : contextToken.kind === SyntaxKind.SlashToken && isJsxClosingElement(contextToken.parent)); case " ": - return !!contextToken && isImportKeyword(contextToken) && contextToken.parent.kind === SyntaxKind.SourceFile; + return !!contextToken && ( + isImportKeyword(contextToken) && contextToken.parent.kind === SyntaxKind.SourceFile || + contextToken.kind === SyntaxKind.CaseKeyword); default: return Debug.assertNever(triggerCharacter); } diff --git a/tests/cases/fourslash/contextualTypeInSwitch.ts b/tests/cases/fourslash/contextualTypeInSwitch.ts new file mode 100644 index 0000000000000..92671ce4b5f2d --- /dev/null +++ b/tests/cases/fourslash/contextualTypeInSwitch.ts @@ -0,0 +1,80 @@ +/// + +//// enum E { +//// A = 0, +//// B = "B", +//// C = "C", +//// } +//// declare const u: E.A | E.B | 1; +//// switch (u) { +//// case /*1*/ +//// } +//// 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: "A", + // sortText: completion.SortText.LocationPriority, + // }, + // { + // name: "B", + // sortText: completion.SortText.LocationPriority, + // } + ], + excludes: [ + // >> TODO: exclude C + ], + }, + { + marker: "2", + isNewIdentifierLocation: false, + includes: [ + // { + // name: "A", + // sortText: completion.SortText.LocationPriority, + // }, + // { + // name: "B", + // sortText: completion.SortText.LocationPriority, + // }, + // { + // name: "C", + // sortText: completion.SortText.LocationPriority, + // } + ], + }, + { + marker: "3", + isNewIdentifierLocation: false, + includes: [ + // { + // name: "D", + // sortText: completion.SortText.LocationPriority, + // }, + // { + // name: "E", + // sortText: completion.SortText.LocationPriority, + // }, + // { + // name: "F", + // sortText: completion.SortText.LocationPriority, + // } + ], + }, +); \ No newline at end of file From 73c1eea62c7027b527d99cd6fa2873acef8e9553 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 16 Sep 2022 11:13:03 -0700 Subject: [PATCH 04/23] use symbol to expression. TODO: filter existing, replace import nodes --- src/compiler/checker.ts | 14 +- src/compiler/utilities.ts | 7 + src/services/completions.ts | 181 +++++++++++++----- ...ypeInSwitch.ts => fullCaseCompletions1.ts} | 11 +- tests/cases/fourslash/fullCaseCompletions2.ts | 88 +++++++++ 5 files changed, 250 insertions(+), 51 deletions(-) rename tests/cases/fourslash/{contextualTypeInSwitch.ts => fullCaseCompletions1.ts} (83%) create mode 100644 tests/cases/fourslash/fullCaseCompletions2.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index dd56dc8ff8905..d6daf09dc4299 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6611,10 +6611,7 @@ namespace ts { 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; @@ -39764,7 +39761,7 @@ namespace ts { // Grammar checking checkGrammarStatementInAmbientContext(node); - let firstDefaultClause: CaseOrDefaultClause; + let firstDefaultClause: CaseOrDefaultClause | undefined = undefined; let hasDuplicateDefaultClause = false; const expressionType = checkExpression(node.expression); @@ -39808,6 +39805,13 @@ namespace ts { }; } }); + + const lastCaseClause = !firstDefaultClause && last(node.caseBlock.clauses); + if (lastCaseClause) { + // >> TODO: check if switch expression is never + // >> TODO: I'd need to learn how the flow graph is built to check this correctly, if there's even a way + } + if (node.caseBlock.locals) { registerForUnusedIdentifiersCheck(node.caseBlock); } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index cdc0db9407b34..84c9718c715e5 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7752,4 +7752,11 @@ namespace ts { export function getParameterTypeNode(parameter: ParameterDeclaration | JSDocParameterTag) { return parameter.kind === SyntaxKind.JSDocParameterTag ? parameter.typeExpression?.type : parameter.type; } + + export function canUsePropertyAccess(name: string, languageVersion: ScriptTarget): boolean { + const firstChar = name.charCodeAt(0); + return firstChar === CharacterCodes.hash ? + name.length > 1 && isIdentifierStart(name.charCodeAt(1), languageVersion) : + isIdentifierStart(firstChar, languageVersion); + } } diff --git a/src/services/completions.ts b/src/services/completions.ts index bfde032dea331..afa6f03382dc8 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -53,6 +53,8 @@ namespace ts.Completions { TypeOnlyAlias = "TypeOnlyAlias/", /** Auto-import that comes attached to an object literal method snippet */ ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/", + /** Case completions for switch statements */ + SwitchCases = "SwitchCases/", } const enum SymbolOriginInfoKind { @@ -567,9 +569,9 @@ namespace ts.Completions { // >> TODO: prototype remaining cases here if (contextToken && isCaseKeyword(contextToken) && preferences.includeCompletionsWithInsertText) { - const casesEntry = getExhaustiveCaseSnippets(contextToken, sourceFile, preferences, compilerOptions, host, program.getTypeChecker(), formatContext); - if (casesEntry) { - entries.push(casesEntry); + const cases = getExhaustiveCaseSnippets(contextToken, sourceFile, preferences, compilerOptions, host, program, formatContext); + if (cases) { + entries.push(cases.entry); } } @@ -594,58 +596,89 @@ namespace ts.Completions { preferences: UserPreferences, options: CompilerOptions, host: LanguageServiceHost, - checker: TypeChecker, - formatContext: formatting.FormatContext | undefined): CompletionEntry | undefined { + program: Program, + formatContext: formatting.FormatContext | undefined): { entry: CompletionEntry, importAdder: codefix.ImportAdder } | undefined { const caseClause = tryCast(contextToken.parent, isCaseClause); if (caseClause) { + const checker = program.getTypeChecker(); + // >> TODO: only offer if we're not positioned *after* a default clause const switchType = getSwitchedType(caseClause, checker); if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) { // >> TODO: does this work for enum members? - const elements: [string, Expression][] = mapDefined(switchType.types as LiteralType[], type => { + const printer = createSnippetPrinter({ + removeComments: true, + module: options.module, + target: options.target, + newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), + }); + + // const existingClauses = new Map(mapDefined(caseClause.parent.clauses, clause => { + // if (isDefaultClause(clause)) { + // return undefined; + // } + // const symbol = checker.getSymbolAtLocation(clause.expression); // >> TODO: what if it's a literal? + // if (symbol) { + // return [getSymbolId(symbol), true]; + // } + // return undefined; + // })); + + const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); + let hasUnrecognizedType = false; + let elements: Expression[] = mapDefined(switchType.types as LiteralType[], type => { if (type.flags & TypeFlags.EnumLiteral) { Debug.assert(type.symbol, "TODO: should this hold always?"); Debug.assert(type.symbol.parent, "TODO: should this hold always too?"); - const target = getEmitScriptTarget(options); - // >> TODO: figure out if need an import action - const memberInfo = getCompletionEntryDisplayNameForSymbol( - type.symbol, - target, - /*origin*/ undefined, - CompletionKind.None, /*jsxIdentifierExpected*/ false); - const enumInfo = getCompletionEntryDisplayNameForSymbol( - type.symbol.parent, - target, - /*origin*/ undefined, - CompletionKind.None, /*jsxIdentifierExpected*/ false); - if (memberInfo && enumInfo) { - const enumExp = factory.createIdentifier(enumInfo.name); // >> TODO: have to properly get the exp - const access: Expression = factory.createPropertyAccessExpression(enumExp, memberInfo.name); // >> TODO: we might need to use [] to access this member - // >> TODO: convert access to text properly - return [`${enumInfo.name}.${memberInfo.name}`, access]; // >> TODO: this looks hacky, do it some other way - } + // if (!existingClauses.has(getSymbolId(type.symbol))) { + // const target = getEmitScriptTarget(options); + // >> TODO: figure out if need an import action + // const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseClause.parent, target); + // if (typeNode) { + // const typeNodeText = printer.printSnippetList( + // ListFormat.None, + // factory.createNodeArray([typeNode]), + // sourceFile); + // typeNodeText; + + // const expr = foo(typeNode, target); + // if (expr) { + // return [typeNodeText, expr]; // >> TODO: how do we convert this? + // } + + // } + const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined); + if (expr) { + return expr; + } + // } } else { - const text = completionNameForLiteral(sourceFile, preferences, type.value); + // const text = completionNameForLiteral(sourceFile, preferences, type.value); const literal: Expression = typeof type.value === "object" ? factory.createBigIntLiteral(type.value) : typeof type.value === "number" ? factory.createNumericLiteral(type.value) : factory.createStringLiteral(type.value); - return [text, literal]; + return literal; } - return undefined; // >> TODO: actually only do this if every return is defined, - // >> otherwise won't really be exhaustive + hasUnrecognizedType = true; + return undefined; }); - const clauses = map(elements, element => { - return factory.createCaseClause(element[1], []); - }); - const printer = createSnippetPrinter({ - removeComments: true, - module: options.module, - target: options.target, - newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), + if (hasUnrecognizedType || elements.length === 0) { + return undefined; + } + + // const existingClauses = new Set(mapDefined(caseClause.parent.clauses, clause => { + // if (isDefaultClause(clause)) { + // return undefined; + // } + // return clause.expression.getText(); + // })); + + // elements = filter(elements, element => !existingClauses.has(element[0])); + const clauses = mapDefined(elements, element => { + return factory.createCaseClause(element, []); }); - // >> TODO: get format context and format before printing, if possible const insertText = formatContext ? printer.printAndFormatSnippetList( ListFormat.MultiLine | ListFormat.NoTrailingNewLine, @@ -657,21 +690,52 @@ namespace ts.Completions { factory.createNodeArray(clauses), sourceFile); + const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([first(clauses)!]), sourceFile); return { - name: elements[0][0], - isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one - kind: ScriptElementKind.unknown, // >> TODO: what should this be? - sortText: SortText.LocalDeclarationPriority, - insertText, - replacementSpan: getReplacementSpanForContextToken(contextToken), + entry: { + name: `${firstClause} ...`, // >> TODO: what should this be? + isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one + kind: ScriptElementKind.unknown, // >> TODO: what should this be? + sortText: SortText.LocalDeclarationPriority, + insertText, + replacementSpan: getReplacementSpanForContextToken(contextToken), + hasAction: importAdder.hasFixes() || undefined, + source: CompletionSource.SwitchCases, + }, + importAdder, } - // >> TODO: filter cases that are already there } } return undefined; } + // function foo(typeNode: TypeNode, languageVersion: ScriptTarget): Expression | undefined { + // switch (typeNode.kind) { + // case SyntaxKind.TypeReference: + // const typeName = (typeNode as TypeReferenceNode).typeName; + // return bar(typeName); + // // case SyntaxKind.ImportType: // >> TODO: We shouldn't have any import type + // case SyntaxKind.IndexedAccessType: + // return undefined; // >> TODO: do we need this case? + // } + + // return undefined; + + // function bar(entityName: EntityName): Expression { + // if (isIdentifier(entityName)) { + // return entityName; + // } + // const realName = unescapeLeadingUnderscores(entityName.right.escapedText); + // if (canUsePropertyAccess(realName, languageVersion)) { + // return factory.createPropertyAccessExpression(bar(entityName.left), realName); + // } + // else { + // return factory.createElementAccessExpression(bar(entityName.left), factory.createStringLiteral(`"${realName}"`)); // >> TODO: this is wrong + // } + // } + // } + function isMemberCompletionKind(kind: CompletionKind): boolean { switch (kind) { case CompletionKind.ObjectPropertyDeclaration: @@ -1668,7 +1732,10 @@ namespace ts.Completions { 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) { @@ -1769,6 +1836,30 @@ namespace ts.Completions { 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 as CaseKeyword, + sourceFile, + preferences, + program.getCompilerOptions(), + host, + program, + /*formatContext*/ undefined)!; + 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]), + }], + }; + } 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/tests/cases/fourslash/contextualTypeInSwitch.ts b/tests/cases/fourslash/fullCaseCompletions1.ts similarity index 83% rename from tests/cases/fourslash/contextualTypeInSwitch.ts rename to tests/cases/fourslash/fullCaseCompletions1.ts index 92671ce4b5f2d..871971301a58e 100644 --- a/tests/cases/fourslash/contextualTypeInSwitch.ts +++ b/tests/cases/fourslash/fullCaseCompletions1.ts @@ -11,7 +11,7 @@ //// } //// declare const e: E; //// switch (e) { -//// case /*2*/ +//// case /*2*/ //// } //// enum F { //// D = 1 << 0, @@ -40,6 +40,9 @@ verify.completions( excludes: [ // >> TODO: exclude C ], + preferences: { + includeCompletionsWithInsertText: true, + }, }, { marker: "2", @@ -58,6 +61,9 @@ verify.completions( // sortText: completion.SortText.LocationPriority, // } ], + preferences: { + includeCompletionsWithInsertText: true, + }, }, { marker: "3", @@ -76,5 +82,8 @@ verify.completions( // sortText: completion.SortText.LocationPriority, // } ], + preferences: { + includeCompletionsWithInsertText: true, + }, }, ); \ No newline at end of file diff --git a/tests/cases/fourslash/fullCaseCompletions2.ts b/tests/cases/fourslash/fullCaseCompletions2.ts new file mode 100644 index 0000000000000..834155df0cd12 --- /dev/null +++ b/tests/cases/fourslash/fullCaseCompletions2.ts @@ -0,0 +1,88 @@ +/// + +// @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"; +//// switch (d.u) { +//// case /*2*/ +//// } + +verify.completions( + { + marker: "1", + isNewIdentifierLocation: false, + includes: [ + // { + // name: "A", + // sortText: completion.SortText.LocationPriority, + // }, + // { + // name: "B", + // sortText: completion.SortText.LocationPriority, + // } + ], + excludes: [ + // >> TODO: exclude C + ], + preferences: { + includeCompletionsWithInsertText: true, + }, + }, + { + marker: "2", + isNewIdentifierLocation: false, + includes: [ + // { + // name: "A", + // sortText: completion.SortText.LocationPriority, + // }, + // { + // name: "B", + // sortText: completion.SortText.LocationPriority, + // } + ], + excludes: [ + // >> TODO: exclude 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 +// }`, +// }) + +// verify.applyCodeActionFromCompletion("2", { +// name: "case E.A: ...", +// source: "SwitchCases/", +// description: "Includes imports of types referenced by 'case E.A: ...'", +// newFileContent: +// `import * as d from "./dep"; +// import { E, u } from "./dep"; +// switch (d.u) { +// case +// }`, +// }) \ No newline at end of file From 5648cbafb2dffb8d098e9f3d15a00590b74c5f16 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 23 Sep 2022 15:45:07 -0700 Subject: [PATCH 05/23] WIP --- src/services/completions.ts | 173 +++++++++++------- tests/cases/fourslash/fullCaseCompletions3.ts | 65 +++++++ 2 files changed, 173 insertions(+), 65 deletions(-) create mode 100644 tests/cases/fourslash/fullCaseCompletions3.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index afa6f03382dc8..04d116e85a70e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -600,10 +600,18 @@ namespace ts.Completions { formatContext: formatting.FormatContext | undefined): { entry: CompletionEntry, importAdder: codefix.ImportAdder } | undefined { const caseClause = tryCast(contextToken.parent, isCaseClause); if (caseClause) { + // Only offer this completion if we're not positioned *after* a default clause + const clauses = caseClause.parent.clauses; + const defaultClauseIndex = findIndex(clauses, isDefaultClause); + const currentIndex = findIndex(clauses, c => c === caseClause); + if (defaultClauseIndex !== -1 && currentIndex > defaultClauseIndex) { + return undefined; + } const checker = program.getTypeChecker(); - // >> TODO: only offer if we're not positioned *after* a default clause const switchType = getSwitchedType(caseClause, checker); - if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) { // >> TODO: does this work for enum members? + // >> TODO: use `isTypeLiteral` from checker? + // >> that considers unions of literals, all unit types, and also booleans. + if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) {// >> TODO: does this work for enum members? aliases? const printer = createSnippetPrinter({ removeComments: true, module: options.module, @@ -611,45 +619,80 @@ namespace ts.Completions { newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), }); - // const existingClauses = new Map(mapDefined(caseClause.parent.clauses, clause => { - // if (isDefaultClause(clause)) { - // return undefined; - // } - // const symbol = checker.getSymbolAtLocation(clause.expression); // >> TODO: what if it's a literal? - // if (symbol) { - // return [getSymbolId(symbol), true]; - // } - // return undefined; - // })); + + // >> TODO: not sure if this is fast enough for filtering existing cases + // const existingSymbols = new Map(); + // const existingLiterals: (number | string | PseudoBigInt)[] = []; + // existingLiterals; + const existingValues: (string | number | PseudoBigInt)[] = []; + + for (const clause of clauses) { + if (isDefaultClause(clause)) { + continue; + } + if (isLiteralExpression(clause.expression)) { + const expression = clause.expression; + switch (expression.kind) { + case SyntaxKind.StringLiteral: + existingValues.push(expression.text); + continue; + case SyntaxKind.NumericLiteral: + existingValues.push(parseInt(expression.text)); // ?? + continue; + case SyntaxKind.BigIntLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + continue; + default: + return undefined; + } + } + else { + const symbol = checker.getSymbolAtLocation(clause.expression); + if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) { + // TODO: check that it's an enum member symbol + const enumValue = checker.getConstantValue(symbol.valueDeclaration); + if (enumValue) { + existingValues.push(enumValue); + } + else { + return undefined + } + } + } + return undefined; // We couldn't recognize all existing case clauses, so fail. + } + const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); - let hasUnrecognizedType = false; - let elements: Expression[] = mapDefined(switchType.types as LiteralType[], type => { + const elements: Expression[] = []; + for (const type of switchType.types as LiteralType[]) { if (type.flags & TypeFlags.EnumLiteral) { Debug.assert(type.symbol, "TODO: should this hold always?"); Debug.assert(type.symbol.parent, "TODO: should this hold always too?"); - // if (!existingClauses.has(getSymbolId(type.symbol))) { - // const target = getEmitScriptTarget(options); - // >> TODO: figure out if need an import action - // const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseClause.parent, target); - // if (typeNode) { - // const typeNodeText = printer.printSnippetList( - // ListFormat.None, - // factory.createNodeArray([typeNode]), - // sourceFile); - // typeNodeText; - - // const expr = foo(typeNode, target); - // if (expr) { - // return [typeNodeText, expr]; // >> TODO: how do we convert this? - // } - - // } - const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined); + // if (existingSymbols.has(getSymbolId(type.symbol))) { + // continue; + // } + const target = getEmitScriptTarget(options); + // >> TODO: figure out if need an import action + const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseClause.parent, target); + if (typeNode) { + const typeNodeText = printer.printSnippetList( + ListFormat.None, + factory.createNodeArray([typeNode]), + sourceFile); + typeNodeText; + + const expr = foo(typeNode, target); if (expr) { - return expr; + elements.push(expr); } + } + // >> TODO: what if expression has import node? + // const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined); + // if (expr) { + // return expr; // } + // } } else { // const text = completionNameForLiteral(sourceFile, preferences, type.value); @@ -658,13 +701,12 @@ namespace ts.Completions { : typeof type.value === "number" ? factory.createNumericLiteral(type.value) : factory.createStringLiteral(type.value); - return literal; + elements.push(literal); } - hasUnrecognizedType = true; return undefined; - }); + } - if (hasUnrecognizedType || elements.length === 0) { + if (elements.length === 0) { return undefined; } @@ -676,18 +718,18 @@ namespace ts.Completions { // })); // elements = filter(elements, element => !existingClauses.has(element[0])); - const clauses = mapDefined(elements, element => { + const newClauses = mapDefined(elements, element => { return factory.createCaseClause(element, []); }); const insertText = formatContext ? printer.printAndFormatSnippetList( ListFormat.MultiLine | ListFormat.NoTrailingNewLine, - factory.createNodeArray(clauses), + factory.createNodeArray(newClauses), sourceFile, formatContext) : printer.printSnippetList( ListFormat.MultiLine | ListFormat.NoTrailingNewLine, - factory.createNodeArray(clauses), + factory.createNodeArray(newClauses), sourceFile); const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([first(clauses)!]), sourceFile); @@ -710,31 +752,32 @@ namespace ts.Completions { return undefined; } - // function foo(typeNode: TypeNode, languageVersion: ScriptTarget): Expression | undefined { - // switch (typeNode.kind) { - // case SyntaxKind.TypeReference: - // const typeName = (typeNode as TypeReferenceNode).typeName; - // return bar(typeName); - // // case SyntaxKind.ImportType: // >> TODO: We shouldn't have any import type - // case SyntaxKind.IndexedAccessType: - // return undefined; // >> TODO: do we need this case? - // } - - // return undefined; - - // function bar(entityName: EntityName): Expression { - // if (isIdentifier(entityName)) { - // return entityName; - // } - // const realName = unescapeLeadingUnderscores(entityName.right.escapedText); - // if (canUsePropertyAccess(realName, languageVersion)) { - // return factory.createPropertyAccessExpression(bar(entityName.left), realName); - // } - // else { - // return factory.createElementAccessExpression(bar(entityName.left), factory.createStringLiteral(`"${realName}"`)); // >> TODO: this is wrong - // } - // } - // } + function foo(typeNode: TypeNode, languageVersion: ScriptTarget): Expression | undefined { + switch (typeNode.kind) { + case SyntaxKind.TypeReference: + const typeName = (typeNode as TypeReferenceNode).typeName; + return bar(typeName); + case SyntaxKind.ImportType: + Debug.fail(`We should not get an import type after calling 'codefix.typeToAutoImportableTypeNode'.`); + case SyntaxKind.IndexedAccessType: + return undefined; // >> TODO: do we need this case? + } + + return undefined; + + function bar(entityName: EntityName): Expression { + if (isIdentifier(entityName)) { + return entityName; + } + const realName = unescapeLeadingUnderscores(entityName.right.escapedText); + if (canUsePropertyAccess(realName, languageVersion)) { + return factory.createPropertyAccessExpression(bar(entityName.left), realName); + } + else { + return factory.createElementAccessExpression(bar(entityName.left), factory.createStringLiteral(`"${realName}"`)); // >> TODO: this is wrong + } + } + } function isMemberCompletionKind(kind: CompletionKind): boolean { switch (kind) { diff --git a/tests/cases/fourslash/fullCaseCompletions3.ts b/tests/cases/fourslash/fullCaseCompletions3.ts new file mode 100644 index 0000000000000..e0fce4c6e290d --- /dev/null +++ b/tests/cases/fourslash/fullCaseCompletions3.ts @@ -0,0 +1,65 @@ +/// + + +// @Filename: /main.ts +//// enum E { +//// A = 0, +//// B = "B", +//// C = "C", +//// } +//// declare const u: E.A | E.B | 1 | 1n | "1"; +//// switch (u) { +//// case E.A: +//// case 1: +//// case 1n: +//// case "1": +//// case `1`: +//// case `1${u}`: +//// case /*1*/ +//// } + +verify.completions( + { + marker: "1", + isNewIdentifierLocation: false, + includes: [ + // { + // name: "A", + // sortText: completion.SortText.LocationPriority, + // }, + // { + // name: "B", + // sortText: completion.SortText.LocationPriority, + // } + ], + excludes: [ + // >> TODO: exclude 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 +// }`, +// }) + +// verify.applyCodeActionFromCompletion("2", { +// name: "case E.A: ...", +// source: "SwitchCases/", +// description: "Includes imports of types referenced by 'case E.A: ...'", +// newFileContent: +// `import * as d from "./dep"; +// import { E, u } from "./dep"; +// switch (d.u) { +// case +// }`, +// }) \ No newline at end of file From d46c0d254d6093e3375bfee8d1a80aecafd1d23a Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 28 Sep 2022 15:35:16 -0700 Subject: [PATCH 06/23] WIP --- src/compiler/checker.ts | 30 +----- src/compiler/utilities.ts | 42 +++++++++ src/services/completions.ts | 94 ++++++++++--------- tests/cases/fourslash/fullCaseCompletions3.ts | 2 + 4 files changed, 95 insertions(+), 73 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d6daf09dc4299..e825934a6608e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22449,35 +22449,7 @@ namespace ts { * @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 84c9718c715e5..3569d986c8962 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7313,6 +7313,48 @@ namespace ts { return (negative && base10Value !== "0" ? "-" : "") + base10Value; } + export function parseBigInt(text: string): PseudoBigInt | undefined { + if (!isValidBigIntString(text, /*roundTripOnly*/ false)) { + return undefined; + } + return parseValidBigInt(text); + } + + /** + * @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 }; + } + + /** + * 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()) })); + } + export function isValidTypeOnlyAliasUseSite(useSite: Node): boolean { return !!(useSite.flags & NodeFlags.Ambient) || isPartOfTypeQuery(useSite) diff --git a/src/services/completions.ts b/src/services/completions.ts index 04d116e85a70e..fee6cf8f3fcdc 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -567,7 +567,7 @@ namespace ts.Completions { getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries); } - // >> TODO: prototype remaining cases here + // >> TODO: trigger on semi-completed case-keyword if (contextToken && isCaseKeyword(contextToken) && preferences.includeCompletionsWithInsertText) { const cases = getExhaustiveCaseSnippets(contextToken, sourceFile, preferences, compilerOptions, host, program, formatContext); if (cases) { @@ -624,7 +624,11 @@ namespace ts.Completions { // const existingSymbols = new Map(); // const existingLiterals: (number | string | PseudoBigInt)[] = []; // existingLiterals; - const existingValues: (string | number | PseudoBigInt)[] = []; + // const existingValues: (string | number | PseudoBigInt)[] = []; + const existingStrings = new Set(); + const existingNumbers = new Set(); + const existingBigInts = new Set(); + const existingBools = new Set(); for (const clause of clauses) { if (isDefaultClause(clause)) { @@ -633,69 +637,80 @@ namespace ts.Completions { if (isLiteralExpression(clause.expression)) { const expression = clause.expression; switch (expression.kind) { + case SyntaxKind.NoSubstitutionTemplateLiteral: case SyntaxKind.StringLiteral: - existingValues.push(expression.text); - continue; + existingStrings.add(expression.text); + break; case SyntaxKind.NumericLiteral: - existingValues.push(parseInt(expression.text)); // ?? - continue; + existingNumbers.add(parseInt(expression.text)); // >> do we need to parse it?? + break; case SyntaxKind.BigIntLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - continue; - default: - return undefined; + const parsedBigInt = parseBigInt(endsWith(expression.text, "n") ? expression.text.slice(0, -1) : expression.text); + if (parsedBigInt) { + existingBigInts.add(pseudoBigIntToString(parsedBigInt)); // >> does it work? answer: no + } + break; + case SyntaxKind.TrueKeyword: + existingBools.add(true); + break; + case SyntaxKind.FalseKeyword: + existingBools.add(false); + break; } } - else { + else if (checker.getSymbolAtLocation(clause.expression)) { const symbol = checker.getSymbolAtLocation(clause.expression); if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) { // TODO: check that it's an enum member symbol const enumValue = checker.getConstantValue(symbol.valueDeclaration); - if (enumValue) { - existingValues.push(enumValue); - } - else { - return undefined + if (enumValue !== undefined) { + switch (typeof enumValue) { + case "string": + existingStrings.add(enumValue); + break; + case "number": + existingNumbers.add(enumValue); + } } } } - return undefined; // We couldn't recognize all existing case clauses, so fail. } - const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); const elements: Expression[] = []; for (const type of switchType.types as LiteralType[]) { if (type.flags & TypeFlags.EnumLiteral) { Debug.assert(type.symbol, "TODO: should this hold always?"); Debug.assert(type.symbol.parent, "TODO: should this hold always too?"); - // if (existingSymbols.has(getSymbolId(type.symbol))) { - // continue; - // } + // >> TODO: see if we need to filter enum by their constant vals const target = getEmitScriptTarget(options); // >> TODO: figure out if need an import action + // >> TODO: fix issue when qualified import const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseClause.parent, target); - if (typeNode) { - const typeNodeText = printer.printSnippetList( - ListFormat.None, - factory.createNodeArray([typeNode]), - sourceFile); - typeNodeText; - - const expr = foo(typeNode, target); - if (expr) { - elements.push(expr); - } + if (!typeNode) { + return undefined; } + const typeNodeText = printer.printSnippetList( + ListFormat.None, + factory.createNodeArray([typeNode]), + sourceFile); + typeNodeText; + + const expr = foo(typeNode, target); + if (!expr) { + return undefined; + } + elements.push(expr); // >> TODO: what if expression has import node? // const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined); // if (expr) { // return expr; // } // } - } + } // >> TODO: else if boolean??? else { // const text = completionNameForLiteral(sourceFile, preferences, type.value); + // >> TODO: filter by existing const literal: Expression = typeof type.value === "object" ? factory.createBigIntLiteral(type.value) : typeof type.value === "number" @@ -703,21 +718,12 @@ namespace ts.Completions { : factory.createStringLiteral(type.value); elements.push(literal); } - return undefined; } if (elements.length === 0) { return undefined; } - // const existingClauses = new Set(mapDefined(caseClause.parent.clauses, clause => { - // if (isDefaultClause(clause)) { - // return undefined; - // } - // return clause.expression.getText(); - // })); - - // elements = filter(elements, element => !existingClauses.has(element[0])); const newClauses = mapDefined(elements, element => { return factory.createCaseClause(element, []); }); @@ -736,9 +742,9 @@ namespace ts.Completions { return { entry: { name: `${firstClause} ...`, // >> TODO: what should this be? - isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one + // isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one kind: ScriptElementKind.unknown, // >> TODO: what should this be? - sortText: SortText.LocalDeclarationPriority, + sortText: SortText.LocalDeclarationPriority, // >> TODO: sort *right after* case keyword insertText, replacementSpan: getReplacementSpanForContextToken(contextToken), hasAction: importAdder.hasFixes() || undefined, diff --git a/tests/cases/fourslash/fullCaseCompletions3.ts b/tests/cases/fourslash/fullCaseCompletions3.ts index e0fce4c6e290d..3ea14a1f900ca 100644 --- a/tests/cases/fourslash/fullCaseCompletions3.ts +++ b/tests/cases/fourslash/fullCaseCompletions3.ts @@ -12,6 +12,7 @@ //// case E.A: //// case 1: //// case 1n: +//// case 0x1n: //// case "1": //// case `1`: //// case `1${u}`: @@ -41,6 +42,7 @@ verify.completions( }, ); + // verify.applyCodeActionFromCompletion("1", { // name: "case E.A: ...", // source: "SwitchCases/", From 297f892ef2e9d46ea4eafec41bea11642eadf7b9 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 28 Sep 2022 15:46:58 -0700 Subject: [PATCH 07/23] remove booleans from literals --- src/services/services.ts | 2 +- tests/cases/fourslash/completionsLiterals.ts | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/services/services.ts b/src/services/services.ts index 080711bff5781..1904f3b2942ad 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -511,7 +511,7 @@ namespace ts { return !!(this.flags & TypeFlags.UnionOrIntersection); } isLiteral(): this is LiteralType { - return !!(this.flags & TypeFlags.Literal); + return !!(this.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral | TypeFlags.BigIntLiteral)); } isStringLiteral(): this is StringLiteralType { return !!(this.flags & TypeFlags.StringLiteral); diff --git a/tests/cases/fourslash/completionsLiterals.ts b/tests/cases/fourslash/completionsLiterals.ts index 639a837907567..e25976417d381 100644 --- a/tests/cases/fourslash/completionsLiterals.ts +++ b/tests/cases/fourslash/completionsLiterals.ts @@ -1,8 +1,7 @@ /// ////const x: 0 | "one" = /**/; -////const y: 0 | "one" | 1n | true = /*1*/; -////const z: boolean = /*2*/; +////const y: 0 | "one" | 1n = /*1*/; verify.completions({ marker: "", @@ -18,16 +17,6 @@ verify.completions({ { name: "0", kind: "string", text: "0" }, { name: '"one"', kind: "string", text: '"one"' }, { name: "1n", kind: "string", text: "1n" }, - { name: "true", kind: "keyword", text: "true", sortText: completion.SortText.GlobalsOrKeywords }, - ], - isNewIdentifierLocation: true, -}); - -verify.completions({ - marker: "2", - includes: [ - { name: "true", kind: "keyword", text: "true", sortText: completion.SortText.GlobalsOrKeywords }, - { name: "false", kind: "keyword", text: "false", sortText: completion.SortText.GlobalsOrKeywords }, ], isNewIdentifierLocation: true, }); From 4c528b3834784c5d582ade5a2646514185146464 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 29 Sep 2022 14:56:38 -0700 Subject: [PATCH 08/23] trigger at case keyword positions --- src/services/completions.ts | 299 +++++++++++++++++------------------- 1 file changed, 144 insertions(+), 155 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index fee6cf8f3fcdc..801ac54596c20 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -266,13 +266,11 @@ namespace ts.Completions { } if (triggerCharacter === " ") { - // `isValidTrigger` ensures we are at `import |` or `case |`. - if (previousToken && isImportKeyword(previousToken)) { - if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { - return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; - } - return undefined; + // `isValidTrigger` ensures we are at `import |` + if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { + return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; } + return undefined; } // If the request is a continuation of an earlier `isIncomplete` response, @@ -568,8 +566,8 @@ namespace ts.Completions { } // >> TODO: trigger on semi-completed case-keyword - if (contextToken && isCaseKeyword(contextToken) && preferences.includeCompletionsWithInsertText) { - const cases = getExhaustiveCaseSnippets(contextToken, sourceFile, preferences, compilerOptions, host, program, formatContext); + if (contextToken && isCaseBlock(contextToken.parent) && preferences.includeCompletionsWithInsertText) { + const cases = getExhaustiveCaseSnippets(contextToken.parent, sourceFile, preferences, compilerOptions, host, program, formatContext); if (cases) { entries.push(cases.entry); } @@ -591,168 +589,160 @@ namespace ts.Completions { } function getExhaustiveCaseSnippets( - contextToken: Token, + // contextToken: Token, + caseBlock: CaseBlock, sourceFile: SourceFile, preferences: UserPreferences, options: CompilerOptions, host: LanguageServiceHost, program: Program, formatContext: formatting.FormatContext | undefined): { entry: CompletionEntry, importAdder: codefix.ImportAdder } | undefined { - const caseClause = tryCast(contextToken.parent, isCaseClause); - if (caseClause) { - // Only offer this completion if we're not positioned *after* a default clause - const clauses = caseClause.parent.clauses; - const defaultClauseIndex = findIndex(clauses, isDefaultClause); - const currentIndex = findIndex(clauses, c => c === caseClause); - if (defaultClauseIndex !== -1 && currentIndex > defaultClauseIndex) { - return undefined; - } - const checker = program.getTypeChecker(); - const switchType = getSwitchedType(caseClause, checker); - // >> TODO: use `isTypeLiteral` from checker? - // >> that considers unions of literals, all unit types, and also booleans. - if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) {// >> TODO: does this work for enum members? aliases? - const printer = createSnippetPrinter({ - removeComments: true, - module: options.module, - target: options.target, - newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), - }); + const clauses = caseBlock.clauses; + // >> TODO: Only offer this completion if we're not positioned *after* a default clause + // const defaultClauseIndex = findIndex(clauses, isDefaultClause); + // const currentIndex = findIndex(clauses, c => c === caseClause); + // if (defaultClauseIndex !== -1 && currentIndex > defaultClauseIndex) { + // return undefined; + // } + const checker = program.getTypeChecker(); + // const switchType = getSwitchedType(caseClause, checker); + const switchType = checker.getTypeAtLocation(caseBlock.parent.expression); + // >> TODO: handle unit type case? + if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) {// >> TODO: does this work for enum members? aliases? + const printer = createSnippetPrinter({ + removeComments: true, + module: options.module, + target: options.target, + newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), + }); - // >> TODO: not sure if this is fast enough for filtering existing cases - // const existingSymbols = new Map(); - // const existingLiterals: (number | string | PseudoBigInt)[] = []; - // existingLiterals; - // const existingValues: (string | number | PseudoBigInt)[] = []; - const existingStrings = new Set(); - const existingNumbers = new Set(); - const existingBigInts = new Set(); - const existingBools = new Set(); - for (const clause of clauses) { - if (isDefaultClause(clause)) { - continue; - } - 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)); // >> do we need to parse it?? - break; - case SyntaxKind.BigIntLiteral: - const parsedBigInt = parseBigInt(endsWith(expression.text, "n") ? expression.text.slice(0, -1) : expression.text); - if (parsedBigInt) { - existingBigInts.add(pseudoBigIntToString(parsedBigInt)); // >> does it work? answer: no - } - break; - case SyntaxKind.TrueKeyword: - existingBools.add(true); - break; - case SyntaxKind.FalseKeyword: - existingBools.add(false); - break; - } - } - else if (checker.getSymbolAtLocation(clause.expression)) { - const symbol = checker.getSymbolAtLocation(clause.expression); - if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) { - // TODO: check that it's an enum member symbol - const enumValue = checker.getConstantValue(symbol.valueDeclaration); - if (enumValue !== undefined) { - switch (typeof enumValue) { - case "string": - existingStrings.add(enumValue); - break; - case "number": - existingNumbers.add(enumValue); - } + // >> TODO: not sure if this is fast enough for filtering existing cases + // const existingSymbols = new Map(); + // const existingLiterals: (number | string | PseudoBigInt)[] = []; + // existingLiterals; + // const existingValues: (string | number | PseudoBigInt)[] = []; + const existingStrings = new Set(); + const existingNumbers = new Set(); + const existingBigInts = new Set(); + const existingBools = new Set(); + + for (const clause of clauses) { + if (isDefaultClause(clause)) { + continue; + } + 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)); // >> do we need to parse it?? + break; + case SyntaxKind.BigIntLiteral: + const parsedBigInt = parseBigInt(endsWith(expression.text, "n") ? expression.text.slice(0, -1) : expression.text); + if (parsedBigInt) { + existingBigInts.add(pseudoBigIntToString(parsedBigInt)); // >> does it work? answer: no } - } + break; + case SyntaxKind.TrueKeyword: + existingBools.add(true); + break; + case SyntaxKind.FalseKeyword: + existingBools.add(false); + break; } } - - const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); - const elements: Expression[] = []; - for (const type of switchType.types as LiteralType[]) { - if (type.flags & TypeFlags.EnumLiteral) { - Debug.assert(type.symbol, "TODO: should this hold always?"); - Debug.assert(type.symbol.parent, "TODO: should this hold always too?"); - // >> TODO: see if we need to filter enum by their constant vals - const target = getEmitScriptTarget(options); - // >> TODO: figure out if need an import action - // >> TODO: fix issue when qualified import - const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseClause.parent, target); - if (!typeNode) { - return undefined; - } - const typeNodeText = printer.printSnippetList( - ListFormat.None, - factory.createNodeArray([typeNode]), - sourceFile); - typeNodeText; - - const expr = foo(typeNode, target); - if (!expr) { - return undefined; + else if (checker.getSymbolAtLocation(clause.expression)) { + const symbol = checker.getSymbolAtLocation(clause.expression); + if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) { + const enumValue = checker.getConstantValue(symbol.valueDeclaration); + if (enumValue !== undefined) { + switch (typeof enumValue) { + case "string": + existingStrings.add(enumValue); + break; + case "number": + existingNumbers.add(enumValue); + } } - elements.push(expr); - // >> TODO: what if expression has import node? - // const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined); - // if (expr) { - // return expr; - // } - // } - } // >> TODO: else if boolean??? - else { - // const text = completionNameForLiteral(sourceFile, preferences, type.value); - // >> TODO: filter by existing - const literal: Expression = typeof type.value === "object" - ? factory.createBigIntLiteral(type.value) - : typeof type.value === "number" - ? factory.createNumericLiteral(type.value) - : factory.createStringLiteral(type.value); - elements.push(literal); } } + } - if (elements.length === 0) { - return undefined; + const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); + const elements: Expression[] = []; + for (const type of switchType.types as LiteralType[]) { + if (type.flags & TypeFlags.EnumLiteral) { + Debug.assert(type.symbol, "TODO: should this hold always?"); + Debug.assert(type.symbol.parent, "TODO: should this hold always too?"); + // >> TODO: see if we need to filter enum by their constant vals + const target = getEmitScriptTarget(options); + // >> TODO: figure out if need an import action + // >> TODO: fix issue when qualified import + const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseBlock, target); + if (!typeNode) { + return undefined; + } + const expr = foo(typeNode, target); + if (!expr) { + return undefined; + } + elements.push(expr); + // >> TODO: what if expression has import node? + // const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined); + // if (expr) { + // return expr; + // } + // } + } // >> TODO: else if boolean??? + else { + // const text = completionNameForLiteral(sourceFile, preferences, type.value); + // >> TODO: filter by existing + const literal: Expression = typeof type.value === "object" + ? factory.createBigIntLiteral(type.value) + : typeof type.value === "number" + ? factory.createNumericLiteral(type.value) + : factory.createStringLiteral(type.value); + elements.push(literal); } + } - const newClauses = mapDefined(elements, element => { - return factory.createCaseClause(element, []); - }); - const insertText = formatContext - ? printer.printAndFormatSnippetList( - ListFormat.MultiLine | ListFormat.NoTrailingNewLine, - factory.createNodeArray(newClauses), - sourceFile, - formatContext) - : printer.printSnippetList( - ListFormat.MultiLine | ListFormat.NoTrailingNewLine, - factory.createNodeArray(newClauses), - sourceFile); - - const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([first(clauses)!]), sourceFile); - return { - entry: { - name: `${firstClause} ...`, // >> TODO: what should this be? - // isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one - kind: ScriptElementKind.unknown, // >> TODO: what should this be? - sortText: SortText.LocalDeclarationPriority, // >> TODO: sort *right after* case keyword - insertText, - replacementSpan: getReplacementSpanForContextToken(contextToken), - hasAction: importAdder.hasFixes() || undefined, - source: CompletionSource.SwitchCases, - }, - importAdder, - } + if (elements.length === 0) { + return undefined; } + + const newClauses = map(elements, element => { + return factory.createCaseClause(element, []); + }); + const insertText = formatContext + ? printer.printAndFormatSnippetList( + ListFormat.MultiLine | ListFormat.NoTrailingNewLine, + factory.createNodeArray(newClauses), + sourceFile, + formatContext) + : printer.printSnippetList( + ListFormat.MultiLine | ListFormat.NoTrailingNewLine, + factory.createNodeArray(newClauses), + sourceFile); + + const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([first(newClauses)!]), sourceFile); + return { + entry: { + name: `${firstClause} ...`, // >> TODO: what should this be? + // isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one + kind: ScriptElementKind.unknown, // >> TODO: what should this be? + sortText: SortText.LocalDeclarationPriority, // >> TODO: sort *right after* case keyword + insertText, + // replacementSpan: getReplacementSpanForContextToken(contextToken), + hasAction: importAdder.hasFixes() || undefined, + source: CompletionSource.SwitchCases, + }, + importAdder, + }; } return undefined; @@ -1887,7 +1877,8 @@ namespace ts.Completions { } case "cases": { const { entry, importAdder } = getExhaustiveCaseSnippets( - contextToken as CaseKeyword, + // contextToken as CaseKeyword, + contextToken!.parent as CaseBlock, sourceFile, preferences, program.getCompilerOptions(), @@ -4424,9 +4415,7 @@ namespace ts.Completions { ? !!tryGetImportFromModuleSpecifier(contextToken) : contextToken.kind === SyntaxKind.SlashToken && isJsxClosingElement(contextToken.parent)); case " ": - return !!contextToken && ( - isImportKeyword(contextToken) && contextToken.parent.kind === SyntaxKind.SourceFile || - contextToken.kind === SyntaxKind.CaseKeyword); + return !!contextToken && isImportKeyword(contextToken) && contextToken.parent.kind === SyntaxKind.SourceFile; default: return Debug.assertNever(triggerCharacter); } From 1a5cd05a9b53acd0a2f144b1b03e2683af4bf9ae Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 7 Nov 2022 17:23:00 -0800 Subject: [PATCH 09/23] clean up tests --- src/services/completions.ts | 109 +++++++------- tests/cases/fourslash/fourslash.ts | 1 + tests/cases/fourslash/fullCaseCompletions1.ts | 68 ++++----- tests/cases/fourslash/fullCaseCompletions2.ts | 83 +++++------ tests/cases/fourslash/fullCaseCompletions3.ts | 138 ++++++++++++------ tests/cases/fourslash/fullCaseCompletions4.ts | 88 +++++++++++ 6 files changed, 305 insertions(+), 182 deletions(-) create mode 100644 tests/cases/fourslash/fullCaseCompletions4.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 801ac54596c20..72d2b2c0d1125 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -565,9 +565,11 @@ namespace ts.Completions { getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries); } - // >> TODO: trigger on semi-completed case-keyword - if (contextToken && isCaseBlock(contextToken.parent) && preferences.includeCompletionsWithInsertText) { - const cases = getExhaustiveCaseSnippets(contextToken.parent, sourceFile, preferences, compilerOptions, host, program, formatContext); + 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); } @@ -589,7 +591,6 @@ namespace ts.Completions { } function getExhaustiveCaseSnippets( - // contextToken: Token, caseBlock: CaseBlock, sourceFile: SourceFile, preferences: UserPreferences, @@ -610,24 +611,11 @@ namespace ts.Completions { const switchType = checker.getTypeAtLocation(caseBlock.parent.expression); // >> TODO: handle unit type case? if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) {// >> TODO: does this work for enum members? aliases? - const printer = createSnippetPrinter({ - removeComments: true, - module: options.module, - target: options.target, - newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), - }); - - - // >> TODO: not sure if this is fast enough for filtering existing cases - // const existingSymbols = new Map(); - // const existingLiterals: (number | string | PseudoBigInt)[] = []; - // existingLiterals; - // const existingValues: (string | number | PseudoBigInt)[] = []; const existingStrings = new Set(); const existingNumbers = new Set(); const existingBigInts = new Set(); - const existingBools = new Set(); + // Collect constant values in existing clauses. for (const clause of clauses) { if (isDefaultClause(clause)) { continue; @@ -648,15 +636,9 @@ namespace ts.Completions { existingBigInts.add(pseudoBigIntToString(parsedBigInt)); // >> does it work? answer: no } break; - case SyntaxKind.TrueKeyword: - existingBools.add(true); - break; - case SyntaxKind.FalseKeyword: - existingBools.add(false); - break; } } - else if (checker.getSymbolAtLocation(clause.expression)) { + else { const symbol = checker.getSymbolAtLocation(clause.expression); if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) { const enumValue = checker.getConstantValue(symbol.valueDeclaration); @@ -673,16 +655,32 @@ namespace ts.Completions { } } + const target = getEmitScriptTarget(options); 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, "TODO: should this hold always?"); Debug.assert(type.symbol.parent, "TODO: should this hold always too?"); - // >> TODO: see if we need to filter enum by their constant vals - const target = getEmitScriptTarget(options); - // >> TODO: figure out if need an import action - // >> TODO: fix issue when qualified import + // Filter existing enums by their values + const enumValue = type.symbol.valueDeclaration && checker.getConstantValue(type.symbol.valueDeclaration as EnumMember); + if (enumValue !== undefined) { + switch (typeof enumValue) { + case "string": + if (existingStrings.has(enumValue)) { + continue; + } + existingStrings.add(enumValue); + break; + case "number": + if (existingNumbers.has(enumValue)) { + continue; + } + existingNumbers.add(enumValue); + break; + } + } const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseBlock, target); if (!typeNode) { return undefined; @@ -692,25 +690,30 @@ namespace ts.Completions { return undefined; } elements.push(expr); - // >> TODO: what if expression has import node? - // const expr = checker.symbolToExpression(type.symbol, SymbolFlags.EnumMember, caseClause.parent, /*flags*/ undefined); - // if (expr) { - // return expr; - // } - // } - } // >> TODO: else if boolean??? + } + // Literals else { - // const text = completionNameForLiteral(sourceFile, preferences, type.value); - // >> TODO: filter by existing - const literal: Expression = typeof type.value === "object" - ? factory.createBigIntLiteral(type.value) - : typeof type.value === "number" - ? factory.createNumericLiteral(type.value) - : factory.createStringLiteral(type.value); - elements.push(literal); + switch (typeof type.value) { + case "object": + // BigInt literal + if (!existingBigInts.has(pseudoBigIntToString(type.value))) { + elements.push(factory.createBigIntLiteral(type.value)); + } + break; + case "number": + // number literal + if (!existingNumbers.has(type.value)) { + elements.push(factory.createNumericLiteral(type.value)); + } + break; + case "string": + if (!existingStrings.has(type.value)) { + elements.push(factory.createStringLiteral(type.value)); + } + break; + } } } - if (elements.length === 0) { return undefined; } @@ -718,6 +721,12 @@ namespace ts.Completions { const newClauses = map(elements, element => { return factory.createCaseClause(element, []); }); + const printer = createSnippetPrinter({ + removeComments: true, + module: options.module, + target: options.target, + newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), + }); const insertText = formatContext ? printer.printAndFormatSnippetList( ListFormat.MultiLine | ListFormat.NoTrailingNewLine, @@ -735,9 +744,8 @@ namespace ts.Completions { name: `${firstClause} ...`, // >> TODO: what should this be? // isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one kind: ScriptElementKind.unknown, // >> TODO: what should this be? - sortText: SortText.LocalDeclarationPriority, // >> TODO: sort *right after* case keyword + sortText: SortText.GlobalsOrKeywords, // >> TODO: sort *right after* case keyword insertText, - // replacementSpan: getReplacementSpanForContextToken(contextToken), hasAction: importAdder.hasFixes() || undefined, source: CompletionSource.SwitchCases, }, @@ -1843,9 +1851,9 @@ namespace ts.Completions { 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. @@ -1877,7 +1885,6 @@ namespace ts.Completions { } case "cases": { const { entry, importAdder } = getExhaustiveCaseSnippets( - // contextToken as CaseKeyword, contextToken!.parent as CaseBlock, sourceFile, preferences, diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 76ca5d33314d7..24c83db39204e 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -879,6 +879,7 @@ declare namespace completion { ClassMemberSnippet = "ClassMemberSnippet/", TypeOnlyAlias = "TypeOnlyAlias/", ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/", + SwitchCases = "SwitchCases/", } export const globalThisEntry: Entry; export const undefinedVarEntry: Entry; diff --git a/tests/cases/fourslash/fullCaseCompletions1.ts b/tests/cases/fourslash/fullCaseCompletions1.ts index 871971301a58e..7dfffb1ce7e89 100644 --- a/tests/cases/fourslash/fullCaseCompletions1.ts +++ b/tests/cases/fourslash/fullCaseCompletions1.ts @@ -1,26 +1,32 @@ /// +// 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*/ +//// case/*1*/ //// } +//// // Union enum //// declare const e: E; //// switch (e) { -//// case /*2*/ +//// case/*2*/ //// } //// enum F { //// D = 1 << 0, //// E = 1 << 1, //// F = 1 << 2, //// } +//// // Computed enum; not supported (TODO: review this after new enum merge) //// declare const f: F; //// switch (f) { -//// case /*3*/ +//// case/*3*/ //// } verify.completions( @@ -28,17 +34,15 @@ verify.completions( marker: "1", isNewIdentifierLocation: false, includes: [ - // { - // name: "A", - // sortText: completion.SortText.LocationPriority, - // }, - // { - // name: "B", - // sortText: completion.SortText.LocationPriority, - // } - ], - excludes: [ - // >> TODO: exclude C + { + name: "case E.A: ...", + source: completion.CompletionSource.SwitchCases, + sortText: completion.SortText.GlobalsOrKeywords, + insertText: +`case E.A: +case E.B: +case 1:`, + }, ], preferences: { includeCompletionsWithInsertText: true, @@ -48,18 +52,15 @@ verify.completions( marker: "2", isNewIdentifierLocation: false, includes: [ - // { - // name: "A", - // sortText: completion.SortText.LocationPriority, - // }, - // { - // name: "B", - // sortText: completion.SortText.LocationPriority, - // }, - // { - // name: "C", - // sortText: completion.SortText.LocationPriority, - // } + { + 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, @@ -68,20 +69,7 @@ verify.completions( { marker: "3", isNewIdentifierLocation: false, - includes: [ - // { - // name: "D", - // sortText: completion.SortText.LocationPriority, - // }, - // { - // name: "E", - // sortText: completion.SortText.LocationPriority, - // }, - // { - // name: "F", - // sortText: completion.SortText.LocationPriority, - // } - ], + exact: ["e", "E", "f", { name: "F", isRecommended: true }, "u", ...completion.globals], preferences: { includeCompletionsWithInsertText: true, }, diff --git a/tests/cases/fourslash/fullCaseCompletions2.ts b/tests/cases/fourslash/fullCaseCompletions2.ts index 834155df0cd12..777cf484b6abc 100644 --- a/tests/cases/fourslash/fullCaseCompletions2.ts +++ b/tests/cases/fourslash/fullCaseCompletions2.ts @@ -1,5 +1,8 @@ /// +// Import-related cases + +// @newline: LF // @Filename: /dep.ts //// export enum E { //// A = 0, @@ -12,13 +15,14 @@ // @Filename: /main.ts //// import { u } from "./dep"; //// switch (u) { -//// case /*1*/ +//// case/*1*/ //// } // @Filename: /other.ts //// import * as d from "./dep"; -//// switch (d.u) { -//// case /*2*/ +//// declare const u: d.E; +//// switch (u) { +//// case/*2*/ //// } verify.completions( @@ -26,17 +30,16 @@ verify.completions( marker: "1", isNewIdentifierLocation: false, includes: [ - // { - // name: "A", - // sortText: completion.SortText.LocationPriority, - // }, - // { - // name: "B", - // sortText: completion.SortText.LocationPriority, - // } - ], - excludes: [ - // >> TODO: exclude C + { + 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, @@ -46,17 +49,15 @@ verify.completions( marker: "2", isNewIdentifierLocation: false, includes: [ - // { - // name: "A", - // sortText: completion.SortText.LocationPriority, - // }, - // { - // name: "B", - // sortText: completion.SortText.LocationPriority, - // } - ], - excludes: [ - // >> TODO: exclude C + { + 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, @@ -64,25 +65,13 @@ verify.completions( }, ); -// 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 -// }`, -// }) - -// verify.applyCodeActionFromCompletion("2", { -// name: "case E.A: ...", -// source: "SwitchCases/", -// description: "Includes imports of types referenced by 'case E.A: ...'", -// newFileContent: -// `import * as d from "./dep"; -// import { E, u } from "./dep"; -// switch (d.u) { -// case -// }`, -// }) \ No newline at end of file +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/fullCaseCompletions3.ts b/tests/cases/fourslash/fullCaseCompletions3.ts index 3ea14a1f900ca..924e0211f512c 100644 --- a/tests/cases/fourslash/fullCaseCompletions3.ts +++ b/tests/cases/fourslash/fullCaseCompletions3.ts @@ -1,67 +1,117 @@ /// +// 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.A | E.B | 1 | 1n | "1"; +//// declare const u: E; +//// switch (u) { +//// case/*1*/ +//// } +//// switch (u) { +//// /*2*/ +//// } //// switch (u) { -//// case E.A: //// case 1: -//// case 1n: -//// case 0x1n: -//// case "1": -//// case `1`: -//// case `1${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: [ - // { - // name: "A", - // sortText: completion.SortText.LocationPriority, - // }, - // { - // name: "B", - // sortText: completion.SortText.LocationPriority, - // } - ], - excludes: [ - // >> TODO: exclude C + exhaustiveCaseCompletion, ], 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 -// }`, -// }) - -// verify.applyCodeActionFromCompletion("2", { -// name: "case E.A: ...", -// source: "SwitchCases/", -// description: "Includes imports of types referenced by 'case E.A: ...'", -// newFileContent: -// `import * as d from "./dep"; -// import { E, u } from "./dep"; -// switch (d.u) { -// case -// }`, -// }) \ No newline at end of file + { + 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, + ], + // exact: [ + // { name: "E", isRecommended: true }, + // "u", + // ...completion.globals, + // ], + preferences: { + includeCompletionsWithInsertText: true, + } + }, + { + marker: "6", + exact: [ + "E", + "u", + ...completion.globals, + exhaustiveCaseCompletion, // TODO: shouldn't be here but is + ], + preferences: { + includeCompletionsWithInsertText: true, + } + }, + { + marker: "7", + includes: [ + exhaustiveCaseCompletion, + ], + preferences: { + includeCompletionsWithInsertText: true, + } + }, +); \ No newline at end of file diff --git a/tests/cases/fourslash/fullCaseCompletions4.ts b/tests/cases/fourslash/fullCaseCompletions4.ts new file mode 100644 index 0000000000000..859c92ba8ba41 --- /dev/null +++ b/tests/cases/fourslash/fullCaseCompletions4.ts @@ -0,0 +1,88 @@ +/// + +// Filter existing values. + +// @newline: LF +//// enum E { +//// A = 0, +//// B = "B", +//// C = "C", +//// } +//// 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*/ +//// } +//// enum F { +//// A = "A", +//// B = "B", +//// C = A, +//// } +//// declare const x: F; +//// switch (x) { +//// /*3*/ +//// } + +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, + }, + }, +); \ No newline at end of file From ee42732e6ddf6ae454dfe66f51fe13142b0fda13 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 9 Nov 2022 09:19:04 -0800 Subject: [PATCH 10/23] fix element access expression case --- src/services/completions.ts | 60 +++++++++++-------- tests/cases/fourslash/fullCaseCompletions5.ts | 35 +++++++++++ 2 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 tests/cases/fourslash/fullCaseCompletions5.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 72d2b2c0d1125..443bcdf80d867 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -600,17 +600,10 @@ namespace ts.Completions { formatContext: formatting.FormatContext | undefined): { entry: CompletionEntry, importAdder: codefix.ImportAdder } | undefined { const clauses = caseBlock.clauses; - // >> TODO: Only offer this completion if we're not positioned *after* a default clause - // const defaultClauseIndex = findIndex(clauses, isDefaultClause); - // const currentIndex = findIndex(clauses, c => c === caseClause); - // if (defaultClauseIndex !== -1 && currentIndex > defaultClauseIndex) { - // return undefined; - // } const checker = program.getTypeChecker(); - // const switchType = getSwitchedType(caseClause, checker); const switchType = checker.getTypeAtLocation(caseBlock.parent.expression); // >> TODO: handle unit type case? - if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) {// >> TODO: does this work for enum members? aliases? + if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) { const existingStrings = new Set(); const existingNumbers = new Set(); const existingBigInts = new Set(); @@ -685,7 +678,7 @@ namespace ts.Completions { if (!typeNode) { return undefined; } - const expr = foo(typeNode, target); + const expr = typeNodeToExpression(typeNode, target); if (!expr) { return undefined; } @@ -742,9 +735,8 @@ namespace ts.Completions { return { entry: { name: `${firstClause} ...`, // >> TODO: what should this be? - // isRecommended: true, // >> I assume that is ok because if there's another recommended, it will be sorted after this one kind: ScriptElementKind.unknown, // >> TODO: what should this be? - sortText: SortText.GlobalsOrKeywords, // >> TODO: sort *right after* case keyword + sortText: SortText.GlobalsOrKeywords, insertText, hasAction: importAdder.hasFixes() || undefined, source: CompletionSource.SwitchCases, @@ -756,30 +748,48 @@ namespace ts.Completions { return undefined; } - function foo(typeNode: TypeNode, languageVersion: ScriptTarget): Expression | undefined { + function typeNodeToExpression(typeNode: TypeNode, languageVersion: ScriptTarget): Expression | undefined { switch (typeNode.kind) { case SyntaxKind.TypeReference: const typeName = (typeNode as TypeReferenceNode).typeName; - return bar(typeName); + return entityNameToExpression(typeName, languageVersion); case SyntaxKind.ImportType: Debug.fail(`We should not get an import type after calling 'codefix.typeToAutoImportableTypeNode'.`); case SyntaxKind.IndexedAccessType: - return undefined; // >> TODO: do we need this case? + const objectExpression = typeNodeToExpression((typeNode as IndexedAccessTypeNode).objectType, languageVersion); + const indexExpression = typeNodeToExpression((typeNode as IndexedAccessTypeNode).indexType, languageVersion); + 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); // >> TODO: where to get 'issinglequote' from? + 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); + return exp && (isIdentifier(exp) ? exp : factory.createParenthesizedExpression(exp)); + case SyntaxKind.TypeQuery: + return entityNameToExpression((typeNode as TypeQueryNode).exprName, languageVersion); } return undefined; + } - function bar(entityName: EntityName): Expression { - if (isIdentifier(entityName)) { - return entityName; - } - const realName = unescapeLeadingUnderscores(entityName.right.escapedText); - if (canUsePropertyAccess(realName, languageVersion)) { - return factory.createPropertyAccessExpression(bar(entityName.left), realName); - } - else { - return factory.createElementAccessExpression(bar(entityName.left), factory.createStringLiteral(`"${realName}"`)); // >> TODO: this is wrong - } + function entityNameToExpression(entityName: EntityName, languageVersion: ScriptTarget): Expression { + if (isIdentifier(entityName)) { + return entityName; + } + const realName = unescapeLeadingUnderscores(entityName.right.escapedText); + if (canUsePropertyAccess(realName, languageVersion)) { + return factory.createPropertyAccessExpression(entityNameToExpression(entityName.left, languageVersion), realName); + } + else { + return factory.createElementAccessExpression(entityNameToExpression(entityName.left, languageVersion), factory.createStringLiteral(`"${realName}"`)); // >> TODO: this is wrong } } diff --git a/tests/cases/fourslash/fullCaseCompletions5.ts b/tests/cases/fourslash/fullCaseCompletions5.ts new file mode 100644 index 0000000000000..eee046c77cf5e --- /dev/null +++ b/tests/cases/fourslash/fullCaseCompletions5.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 From 1819d0badd377a7cf7adcd3161cce7cf177d1829 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 9 Nov 2022 15:41:34 -0800 Subject: [PATCH 11/23] refactor dealing with existing values into a tracker --- src/services/completions.ts | 143 ++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 64 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 443bcdf80d867..42d2f65f124fb 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -604,47 +604,11 @@ namespace ts.Completions { const switchType = checker.getTypeAtLocation(caseBlock.parent.expression); // >> TODO: handle unit type case? if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) { - const existingStrings = new Set(); - const existingNumbers = new Set(); - const existingBigInts = new Set(); - // Collect constant values in existing clauses. + const tracker = newCaseClauseTracker(checker); for (const clause of clauses) { - if (isDefaultClause(clause)) { - continue; - } - 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)); // >> do we need to parse it?? - break; - case SyntaxKind.BigIntLiteral: - const parsedBigInt = parseBigInt(endsWith(expression.text, "n") ? expression.text.slice(0, -1) : expression.text); - if (parsedBigInt) { - existingBigInts.add(pseudoBigIntToString(parsedBigInt)); // >> does it work? answer: no - } - break; - } - } - else { - const symbol = checker.getSymbolAtLocation(clause.expression); - if (symbol && symbol.valueDeclaration && isEnumMember(symbol.valueDeclaration)) { - const enumValue = checker.getConstantValue(symbol.valueDeclaration); - if (enumValue !== undefined) { - switch (typeof enumValue) { - case "string": - existingStrings.add(enumValue); - break; - case "number": - existingNumbers.add(enumValue); - } - } - } + if (!isDefaultClause(clause)) { + tracker.addClause(clause); } } @@ -659,20 +623,10 @@ namespace ts.Completions { // Filter existing enums by their values const enumValue = type.symbol.valueDeclaration && checker.getConstantValue(type.symbol.valueDeclaration as EnumMember); if (enumValue !== undefined) { - switch (typeof enumValue) { - case "string": - if (existingStrings.has(enumValue)) { - continue; - } - existingStrings.add(enumValue); - break; - case "number": - if (existingNumbers.has(enumValue)) { - continue; - } - existingNumbers.add(enumValue); - break; + if (tracker.hasValue(enumValue)) { + continue; } + tracker.addValue(enumValue); } const typeNode = codefix.typeToAutoImportableTypeNode(checker, importAdder, type, caseBlock, target); if (!typeNode) { @@ -685,24 +639,16 @@ namespace ts.Completions { elements.push(expr); } // Literals - else { + else if (!tracker.hasValue(type.value)) { switch (typeof type.value) { case "object": - // BigInt literal - if (!existingBigInts.has(pseudoBigIntToString(type.value))) { - elements.push(factory.createBigIntLiteral(type.value)); - } + elements.push(factory.createBigIntLiteral(type.value)); break; case "number": - // number literal - if (!existingNumbers.has(type.value)) { - elements.push(factory.createNumericLiteral(type.value)); - } + elements.push(factory.createNumericLiteral(type.value)); break; case "string": - if (!existingStrings.has(type.value)) { - elements.push(factory.createStringLiteral(type.value)); - } + elements.push(factory.createStringLiteral(type.value)); break; } } @@ -748,6 +694,75 @@ namespace ts.Completions { return undefined; } + interface CaseClauseTracker { + addClause(clause: CaseClause): void; + addValue(value: string | number): void; + hasValue(value: string | number | PseudoBigInt): boolean; + } + + function newCaseClauseTracker(checker: TypeChecker): CaseClauseTracker { + const existingStrings = new Set(); + const existingNumbers = new Set(); + const existingBigInts = new Set(); + + return { + addValue, + addClause, + hasValue, + }; + + function addClause(clause: CaseClause) { + 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)); // >> TODO: do we need to parse it?? + 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); + } + } + } + } + + 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): Expression | undefined { switch (typeNode.kind) { case SyntaxKind.TypeReference: From bd5b817931029f9995456b8af6326c287c5de894 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 10 Nov 2022 13:14:18 -0800 Subject: [PATCH 12/23] fix merge errors --- src/compiler/checker.ts | 79 ++++++------------------------- src/compiler/factory/nodeTests.ts | 5 -- src/compiler/types.ts | 1 - src/compiler/utilities.ts | 2 +- src/services/completions.ts | 3 +- 5 files changed, 17 insertions(+), 73 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ce4b7dc1e905f..fc07e5c0eb528 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -18,7 +18,7 @@ import { createDiagnosticForFileFromMessageChain, createDiagnosticForNode, createDiagnosticForNodeArray, createDiagnosticForNodeFromMessageChain, createDiagnosticMessageChainFromDiagnostic, createEmptyExports, createFileDiagnostic, createGetCanonicalFileName, createGetSymbolWalker, createPrinter, - createPropertyNameNodeForIdentifierOrLiteral, createScanner, createSymbolTable, createTextWriter, + createPropertyNameNodeForIdentifierOrLiteral, createSymbolTable, createTextWriter, createUnderscoreEscapedMultiMap, Debug, Declaration, DeclarationName, declarationNameToString, DeclarationStatement, DeclarationWithTypeParameterChildren, DeclarationWithTypeParameters, Decorator, deduplicate, DefaultClause, defaultMaximumTruncationLength, DeferredTypeReference, DeleteExpression, Diagnostic, DiagnosticCategory, @@ -101,7 +101,7 @@ import { isFunctionExpressionOrArrowFunction, isFunctionLike, isFunctionLikeDeclaration, isFunctionLikeOrClassStaticBlockDeclaration, isFunctionOrModuleBlock, isFunctionTypeNode, isGeneratedIdentifier, isGetAccessor, isGetAccessorDeclaration, isGetOrSetAccessorDeclaration, isGlobalScopeAugmentation, isHeritageClause, - isIdentifier, isIdentifierStart, isIdentifierText, isIdentifierTypePredicate, isIdentifierTypeReference, + isIdentifier, isIdentifierText, isIdentifierTypePredicate, isIdentifierTypeReference, isIfStatement, isImportCall, isImportClause, isImportDeclaration, isImportEqualsDeclaration, isImportKeyword, isImportOrExportSpecifier, isImportSpecifier, isImportTypeNode, isIndexedAccessTypeNode, isInExpressionContext, isInfinityOrNaNString, isInJSDoc, isInJSFile, isInJsonFile, isInterfaceDeclaration, @@ -195,7 +195,7 @@ import { usingSingleLineStringWriter, VariableDeclaration, VariableDeclarationList, VariableLikeDeclaration, VariableStatement, VarianceFlags, visitEachChild, visitNode, visitNodes, Visitor, VisitResult, VoidExpression, walkUpBindingElementsAndPatterns, walkUpParenthesizedExpressions, walkUpParenthesizedTypes, - walkUpParenthesizedTypesAndGetParentAndChild, WhileStatement, WideningContext, WithStatement, YieldExpression, + walkUpParenthesizedTypesAndGetParentAndChild, WhileStatement, WideningContext, WithStatement, YieldExpression, canUsePropertyAccess, parseValidBigInt, isValidBigIntString, } from "./_namespaces/ts"; import * as performance from "./_namespaces/ts.performance"; import * as moduleSpecifiers from "./_namespaces/ts.moduleSpecifiers"; @@ -6878,12 +6878,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } let firstChar = symbolName.charCodeAt(0); - if (isSingleOrDoubleQuote(firstChar) && some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { - return factory.createStringLiteral(getSpecifierForModuleSymbol(symbol, context)); - } - if (index === 0 || canUsePropertyAccess(symbolName, languageVersion)) { - const identifier = setEmitFlags(factory.createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); - identifier.symbol = symbol; + if (isSingleOrDoubleQuote(firstChar) && some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { + return factory.createStringLiteral(getSpecifierForModuleSymbol(symbol, context)); + } + if (index === 0 || canUsePropertyAccess(symbolName, languageVersion)) { + const identifier = setEmitFlags(factory.createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); + identifier.symbol = symbol; return index > 0 ? factory.createPropertyAccessExpression(createExpressionFromSymbolChain(chain, index - 1), identifier) : identifier; } @@ -6892,17 +6892,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { symbolName = symbolName.substring(1, symbolName.length - 1); firstChar = symbolName.charCodeAt(0); } -<<<<<<< HEAD - if (index === 0 || canUsePropertyAccess(symbolName, languageVersion)) { - const identifier = setEmitFlags(factory.createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); - identifier.symbol = symbol; - - return index > 0 ? factory.createPropertyAccessExpression(createExpressionFromSymbolChain(chain, index - 1), identifier) : identifier; -======= let expression: Expression | undefined; if (isSingleOrDoubleQuote(firstChar) && !(symbol.flags & SymbolFlags.EnumMember)) { expression = factory.createStringLiteral(stripQuotes(symbolName).replace(/\\./g, s => s.substring(1)), firstChar === CharacterCodes.singleQuote); ->>>>>>> main } else if (("" + +symbolName) === symbolName) { expression = factory.createNumericLiteral(+symbolName); @@ -21159,37 +21151,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return false; } -<<<<<<< HEAD - /** - * @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) { - return getBigIntLiteralType(parseValidBigInt(text)); - } - - function isMemberOfStringMapping(source: Type, target: Type): boolean { - if (target.flags & (TypeFlags.String | TypeFlags.AnyOrUnknown)) { - return true; - } - if (target.flags & TypeFlags.TemplateLiteral) { - return isTypeAssignableTo(source, target); - } - if (target.flags & TypeFlags.StringMapping) { - // We need to see whether applying the same mappings of the target - // onto the source would produce an identical type *and* that - // it's compatible with the inner-most non-string-mapped type. - // - // The intuition here is that if same mappings don't affect the source at all, - // and the source is compatible with the unmapped target, then they must - // still reside in the same domain. - const mappingStack = []; - while (target.flags & TypeFlags.StringMapping) { - mappingStack.unshift(target.symbol); - target = (target as StringMappingType).type; - } - const mappedSource = reduceLeft(mappingStack, (memo, value) => getStringMappingType(value, memo), source); - return mappedSource === source && isMemberOfStringMapping(source, target); -======= if (type.flags & TypeFlags.UnionOrIntersection) { return !!forEach((type as IntersectionType).types, typeCouldHaveTopLevelSingletonTypes); } @@ -21198,7 +21159,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const constraint = getConstraintOfType(type); if (constraint && constraint !== type) { return typeCouldHaveTopLevelSingletonTypes(constraint); ->>>>>>> main } } @@ -22731,12 +22691,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return isFinite(n) && (!roundTripOnly || "" + n === s); } - /** - * @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) { - return getBigIntLiteralType(parseValidBigInt(text)); - } + /** + * @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) { + return getBigIntLiteralType(parseValidBigInt(text)); + } function isMemberOfStringMapping(source: Type, target: Type): boolean { if (target.flags & (TypeFlags.String | TypeFlags.Any)) { @@ -40051,18 +40011,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } -<<<<<<< HEAD - function checkSwitchStatement(node: SwitchStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - - let firstDefaultClause: CaseOrDefaultClause | undefined = undefined; - let hasDuplicateDefaultClause = false; -======= function checkSwitchStatement(node: SwitchStatement) { // Grammar checking checkGrammarStatementInAmbientContext(node); ->>>>>>> main let firstDefaultClause: CaseOrDefaultClause; let hasDuplicateDefaultClause = false; diff --git a/src/compiler/factory/nodeTests.ts b/src/compiler/factory/nodeTests.ts index 32a9996a57667..038efb48f1176 100644 --- a/src/compiler/factory/nodeTests.ts +++ b/src/compiler/factory/nodeTests.ts @@ -194,11 +194,6 @@ 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 8a2ec294a5316..5132ad0292909 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8358,7 +8358,6 @@ export interface NodeFactory { // // Compound Nodes // ->>>>>>> main createImmediatelyInvokedFunctionExpression(statements: readonly Statement[]): CallExpression; createImmediatelyInvokedFunctionExpression(statements: readonly Statement[], param: ParameterDeclaration, paramValue: Expression): CallExpression; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f03ec84401427..b806c546acb27 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -90,7 +90,7 @@ import { TypePredicate, TypePredicateKind, TypeReferenceNode, unescapeLeadingUnderscores, UnionOrIntersectionTypeNode, ValidImportTypeNode, VariableDeclaration, VariableDeclarationInitializedTo, VariableDeclarationList, VariableLikeDeclaration, VariableStatement, version, WhileStatement, WithStatement, WriteFileCallback, - WriteFileCallbackData, YieldExpression, + WriteFileCallbackData, YieldExpression, isIdentifierStart, } from "./_namespaces/ts"; /** @internal */ diff --git a/src/services/completions.ts b/src/services/completions.ts index ace99671a8a7a..7a214e31785b1 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -58,7 +58,7 @@ import { SymbolFlags, SymbolId, SyntaxKind, TextChange, textChanges, textPart, TextRange, TextSpan, timestamp, Token, TokenSyntaxKind, tokenToString, tryCast, tryGetImportFromModuleSpecifier, Type, TypeChecker, TypeElement, TypeFlags, typeHasCallOrConstructSignatures, TypeLiteralNode, TypeOnlyAliasDeclaration, unescapeLeadingUnderscores, - UnionReduction, UnionType, UserPreferences, VariableDeclaration, walkUpParenthesizedExpressions, + UnionReduction, UnionType, UserPreferences, VariableDeclaration, walkUpParenthesizedExpressions, CaseBlock, canUsePropertyAccess, CaseClause, endsWith, EntityName, EnumMember, IndexedAccessTypeNode, isCaseBlock, isDefaultClause, isEnumMember, isLiteralExpression, LiteralType, LiteralTypeNode, map, NumericLiteral, ParenthesizedTypeNode, parseBigInt, TypeNode, TypeQueryNode, TypeReferenceNode, } from "./_namespaces/ts"; import { StringCompletions } from "./_namespaces/ts.Completions"; @@ -516,7 +516,6 @@ function keywordToCompletionEntry(keyword: TokenSyntaxKind) { sortText: SortText.GlobalsOrKeywords, }; } ->>>>>>> main function specificKeywordCompletionInfo(entries: readonly CompletionEntry[], isNewIdentifierLocation: boolean): CompletionInfo { return { From f02122bfecf69fa5eff914cf60eaec374e4b85a3 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 10 Nov 2022 15:21:15 -0800 Subject: [PATCH 13/23] cleanup and more tests --- src/services/completions.ts | 9 +- tests/cases/fourslash/fullCaseCompletions1.ts | 14 ++- tests/cases/fourslash/fullCaseCompletions4.ts | 91 +++++++++++++++++++ 3 files changed, 107 insertions(+), 7 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 7a214e31785b1..8a581be50fb5e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -682,7 +682,6 @@ function getExhaustiveCaseSnippets( const clauses = caseBlock.clauses; const checker = program.getTypeChecker(); const switchType = checker.getTypeAtLocation(caseBlock.parent.expression); - // >> TODO: handle unit type case? if (switchType && switchType.isUnion() && every(switchType.types, type => type.isLiteral())) { // Collect constant values in existing clauses. const tracker = newCaseClauseTracker(checker); @@ -698,8 +697,8 @@ function getExhaustiveCaseSnippets( for (const type of switchType.types as LiteralType[]) { // Enums if (type.flags & TypeFlags.EnumLiteral) { - Debug.assert(type.symbol, "TODO: should this hold always?"); - Debug.assert(type.symbol.parent, "TODO: should this hold always too?"); + 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) { @@ -760,8 +759,8 @@ function getExhaustiveCaseSnippets( const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([first(newClauses)!]), sourceFile); return { entry: { - name: `${firstClause} ...`, // >> TODO: what should this be? - kind: ScriptElementKind.unknown, // >> TODO: what should this be? + name: `${firstClause} ...`, + kind: ScriptElementKind.unknown, sortText: SortText.GlobalsOrKeywords, insertText, hasAction: importAdder.hasFixes() || undefined, diff --git a/tests/cases/fourslash/fullCaseCompletions1.ts b/tests/cases/fourslash/fullCaseCompletions1.ts index 7dfffb1ce7e89..fedd8ccf50420 100644 --- a/tests/cases/fourslash/fullCaseCompletions1.ts +++ b/tests/cases/fourslash/fullCaseCompletions1.ts @@ -23,7 +23,7 @@ //// E = 1 << 1, //// F = 1 << 2, //// } -//// // Computed enum; not supported (TODO: review this after new enum merge) +//// //// declare const f: F; //// switch (f) { //// case/*3*/ @@ -69,7 +69,17 @@ case E.C:`, { marker: "3", isNewIdentifierLocation: false, - exact: ["e", "E", "f", { name: "F", isRecommended: true }, "u", ...completion.globals], + 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, }, diff --git a/tests/cases/fourslash/fullCaseCompletions4.ts b/tests/cases/fourslash/fullCaseCompletions4.ts index 859c92ba8ba41..6240b4454f082 100644 --- a/tests/cases/fourslash/fullCaseCompletions4.ts +++ b/tests/cases/fourslash/fullCaseCompletions4.ts @@ -8,6 +8,7 @@ //// B = "B", //// C = "C", //// } +//// // Filtering existing literals //// declare const u: E.A | E.B | 1 | 1n | "1"; //// switch (u) { //// case E.A: @@ -25,6 +26,7 @@ //// case `1`: //// /*2*/ //// } +//// // Filtering repreated enum members //// enum F { //// A = "A", //// B = "B", @@ -34,6 +36,38 @@ //// 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( { @@ -85,4 +119,61 @@ case F.B:`, // no C because C's value is the same as A's 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 From a35bc4a1d85df36602dfc5968c68d394ba8c9b75 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 10 Nov 2022 15:40:59 -0800 Subject: [PATCH 14/23] fix lint errors --- src/compiler/factory/nodeTests.ts | 8 ++++---- src/compiler/utilities.ts | 4 ++-- src/services/completions.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/compiler/factory/nodeTests.ts b/src/compiler/factory/nodeTests.ts index 038efb48f1176..54c1854664382 100644 --- a/src/compiler/factory/nodeTests.ts +++ b/src/compiler/factory/nodeTests.ts @@ -85,10 +85,10 @@ export function isDotDotDotToken(node: Node): node is DotDotDotToken { return node.kind === SyntaxKind.DotDotDotToken; } - /*@internal*/ - export function isCommaToken(node: Node): node is Token { - return node.kind === SyntaxKind.CommaToken; - } +/** @internal*/ +export function isCommaToken(node: Node): node is Token { + return node.kind === SyntaxKind.CommaToken; +} export function isPlusToken(node: Node): node is PlusToken { return node.kind === SyntaxKind.PlusToken; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index b806c546acb27..0f91a63b8fc92 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -8138,9 +8138,9 @@ export function parseBigInt(text: string): PseudoBigInt | undefined { } /** + * @internal * @param text a valid bigint string excluding a trailing `n`, but including a possible prefix `-`. Use `isValidBigIntString(text, roundTripOnly)` before calling this function. */ -/** @internal */ export function parseValidBigInt(text: string): PseudoBigInt { const negative = text.startsWith("-"); const base10Value = parsePseudoBigInt(`${negative ? text.slice(1) : text}n`); @@ -8148,11 +8148,11 @@ export function parseValidBigInt(text: string): PseudoBigInt { } /** + * @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. */ -/** @internal */ export function isValidBigIntString(s: string, roundTripOnly: boolean): boolean { if (s === "") return false; const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false); diff --git a/src/services/completions.ts b/src/services/completions.ts index 8a581be50fb5e..f53300d8d9758 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -847,8 +847,6 @@ function typeNodeToExpression(typeNode: TypeNode, languageVersion: ScriptTarget) case SyntaxKind.TypeReference: const typeName = (typeNode as TypeReferenceNode).typeName; return entityNameToExpression(typeName, languageVersion); - case SyntaxKind.ImportType: - Debug.fail(`We should not get an import type after calling 'codefix.typeToAutoImportableTypeNode'.`); case SyntaxKind.IndexedAccessType: const objectExpression = typeNodeToExpression((typeNode as IndexedAccessTypeNode).objectType, languageVersion); const indexExpression = typeNodeToExpression((typeNode as IndexedAccessTypeNode).indexType, languageVersion); @@ -869,6 +867,8 @@ function typeNodeToExpression(typeNode: TypeNode, languageVersion: ScriptTarget) return exp && (isIdentifier(exp) ? exp : factory.createParenthesizedExpression(exp)); case SyntaxKind.TypeQuery: return entityNameToExpression((typeNode as TypeQueryNode).exprName, languageVersion); + case SyntaxKind.ImportType: + Debug.fail(`We should not get an import type after calling 'codefix.typeToAutoImportableTypeNode'.`); } return undefined; From 83b88f7d592bbfdf60af1a69d062b32c113d3503 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 10 Nov 2022 16:03:40 -0800 Subject: [PATCH 15/23] more merge conflict fixes and cleanup --- src/compiler/factory/nodeTests.ts | 2 +- src/compiler/types.ts | 13 ++- src/compiler/utilities.ts | 2 +- src/services/completions.ts | 82 +++++++++---------- src/services/services.ts | 62 +++++++------- tests/cases/fourslash/fullCaseCompletions3.ts | 7 +- 6 files changed, 81 insertions(+), 87 deletions(-) diff --git a/src/compiler/factory/nodeTests.ts b/src/compiler/factory/nodeTests.ts index 54c1854664382..f391dbb1edb59 100644 --- a/src/compiler/factory/nodeTests.ts +++ b/src/compiler/factory/nodeTests.ts @@ -85,7 +85,7 @@ export function isDotDotDotToken(node: Node): node is DotDotDotToken { return node.kind === SyntaxKind.DotDotDotToken; } -/** @internal*/ +/** @internal */ export function isCommaToken(node: Node): node is Token { return node.kind === SyntaxKind.CommaToken; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5132ad0292909..9d254e2be4b55 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1331,7 +1331,6 @@ 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; @@ -7854,12 +7853,12 @@ export interface NodeFactory { // Signature elements // - createTypeParameterDeclaration(modifiers: readonly Modifier[] | undefined, name: string | Identifier, constraint?: TypeNode, defaultType?: TypeNode): TypeParameterDeclaration; - updateTypeParameterDeclaration(node: TypeParameterDeclaration, modifiers: readonly Modifier[] | undefined, name: Identifier, constraint: TypeNode | undefined, defaultType: TypeNode | undefined): TypeParameterDeclaration; - createParameterDeclaration(modifiers: readonly ModifierLike[] | undefined, dotDotDotToken: DotDotDotToken | undefined, name: string | BindingName, questionToken?: QuestionToken, type?: TypeNode, initializer?: Expression): ParameterDeclaration; - updateParameterDeclaration(node: ParameterDeclaration, modifiers: readonly ModifierLike[] | undefined, dotDotDotToken: DotDotDotToken | undefined, name: string | BindingName, questionToken: QuestionToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined): ParameterDeclaration; - createDecorator(expression: Expression): Decorator; - updateDecorator(node: Decorator, expression: Expression): Decorator; + createTypeParameterDeclaration(modifiers: readonly Modifier[] | undefined, name: string | Identifier, constraint?: TypeNode, defaultType?: TypeNode): TypeParameterDeclaration; + updateTypeParameterDeclaration(node: TypeParameterDeclaration, modifiers: readonly Modifier[] | undefined, name: Identifier, constraint: TypeNode | undefined, defaultType: TypeNode | undefined): TypeParameterDeclaration; + createParameterDeclaration(modifiers: readonly ModifierLike[] | undefined, dotDotDotToken: DotDotDotToken | undefined, name: string | BindingName, questionToken?: QuestionToken, type?: TypeNode, initializer?: Expression): ParameterDeclaration; + updateParameterDeclaration(node: ParameterDeclaration, modifiers: readonly ModifierLike[] | undefined, dotDotDotToken: DotDotDotToken | undefined, name: string | BindingName, questionToken: QuestionToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined): ParameterDeclaration; + createDecorator(expression: Expression): Decorator; + updateDecorator(node: Decorator, expression: Expression): Decorator; // // Type Elements diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 0f91a63b8fc92..c53645ce7afd3 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -8139,7 +8139,7 @@ export function parseBigInt(text: string): PseudoBigInt | undefined { /** * @internal - * @param text a valid bigint string excluding a trailing `n`, but including a possible prefix `-`. Use `isValidBigIntString(text, roundTripOnly)` before calling this function. + * @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("-"); diff --git a/src/services/completions.ts b/src/services/completions.ts index f53300d8d9758..326fd26bac7fd 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -149,13 +149,13 @@ export interface SymbolOriginInfo { fileName?: string; } - interface SymbolOriginInfoExport extends SymbolOriginInfo { - symbolName: string; - moduleSymbol: Symbol; - isDefaultExport: boolean; - exportName: string; - exportMapKey: string; - } +interface SymbolOriginInfoExport extends SymbolOriginInfo { + symbolName: string; + moduleSymbol: Symbol; + isDefaultExport: boolean; + exportName: string; + exportMapKey: string; +} interface SymbolOriginInfoResolvedExport extends SymbolOriginInfo { symbolName: string; @@ -504,9 +504,9 @@ function continuePreviousIncompleteResponse( return previousResponse; } - function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo { - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; - } +function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo { + return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; +} function keywordToCompletionEntry(keyword: TokenSyntaxKind) { return { @@ -799,7 +799,7 @@ function newCaseClauseTracker(checker: TypeChecker): CaseClauseTracker { existingStrings.add(expression.text); break; case SyntaxKind.NumericLiteral: - existingNumbers.add(parseInt(expression.text)); // >> TODO: do we need to parse it?? + existingNumbers.add(parseInt(expression.text)); break; case SyntaxKind.BigIntLiteral: const parsedBigInt = parseBigInt(endsWith(expression.text, "n") ? expression.text.slice(0, -1) : expression.text); @@ -883,7 +883,7 @@ function entityNameToExpression(entityName: EntityName, languageVersion: ScriptT return factory.createPropertyAccessExpression(entityNameToExpression(entityName.left, languageVersion), realName); } else { - return factory.createElementAccessExpression(entityNameToExpression(entityName.left, languageVersion), factory.createStringLiteral(`"${realName}"`)); // >> TODO: this is wrong + return factory.createElementAccessExpression(entityNameToExpression(entityName.left, languageVersion), factory.createStringLiteral(`"${realName}"`)); // >> TODO: this is wrong, use appropriate quotes } } @@ -1122,10 +1122,10 @@ function createCompletionEntry( return undefined; } - if (originIsExport(origin) || originIsResolvedExport(origin)) { - data = originToCompletionEntryData(origin); - hasAction = !importStatementCompletion; - } + if (originIsExport(origin) || originIsResolvedExport(origin)) { + data = originToCompletionEntryData(origin); + hasAction = !importStatementCompletion; + } // TODO(drosen): Right now we just permit *all* semantic meanings when calling // 'getSymbolKind' which is permissible given that it is backwards compatible; but @@ -1846,26 +1846,26 @@ function getLabelStatementCompletions(node: Node): CompletionEntry[] { const uniques = new Map(); let current = node; - while (current) { - if (isFunctionLike(current)) { - break; - } - if (isLabeledStatement(current)) { - const name = current.label.text; - if (!uniques.has(name)) { - uniques.set(name, true); - entries.push({ - name, - kindModifiers: ScriptElementKindModifier.none, - kind: ScriptElementKind.label, - sortText: SortText.LocationPriority - }); - } + while (current) { + if (isFunctionLike(current)) { + break; + } + if (isLabeledStatement(current)) { + const name = current.label.text; + if (!uniques.has(name)) { + uniques.set(name, true); + entries.push({ + name, + kindModifiers: ScriptElementKindModifier.none, + kind: ScriptElementKind.label, + sortText: SortText.LocationPriority + }); } - current = current.parent; } - return entries; + current = current.parent; } + return entries; +} interface SymbolCompletion { type: "symbol"; @@ -1906,14 +1906,14 @@ function getSymbolCompletionFromEntryId( } } - const compilerOptions = program.getCompilerOptions(); - const completionData = getCompletionData(program, log, sourceFile, compilerOptions, position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host, /*formatContext*/ undefined); - if (!completionData) { - return { type: "none" }; - } - if (completionData.kind !== CompletionDataKind.Data) { - return { type: "request", request: completionData }; - } + const compilerOptions = program.getCompilerOptions(); + const completionData = getCompletionData(program, log, sourceFile, compilerOptions, position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host, /*formatContext*/ undefined); + if (!completionData) { + return { type: "none" }; + } + if (completionData.kind !== CompletionDataKind.Data) { + return { type: "request", request: completionData }; + } const { symbols, literals, location, completionKind, symbolToOriginInfoMap, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData; diff --git a/src/services/services.ts b/src/services/services.ts index 4d57b5dc420e8..703b169d60dc6 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -600,17 +600,17 @@ class TypeObject implements Type { } } - class SignatureObject implements Signature { - flags: SignatureFlags; - checker: TypeChecker; - declaration!: SignatureDeclaration; - typeParameters?: TypeParameter[]; - parameters!: Symbol[]; - thisParameter!: Symbol; - resolvedReturnType!: Type; - resolvedTypePredicate: TypePredicate | undefined; - minTypeArgumentCount!: number; - minArgumentCount!: number; +class SignatureObject implements Signature { + flags: SignatureFlags; + checker: TypeChecker; + declaration!: SignatureDeclaration; + typeParameters?: TypeParameter[]; + parameters!: Symbol[]; + thisParameter!: Symbol; + resolvedReturnType!: Type; + resolvedTypePredicate: TypePredicate | undefined; + minTypeArgumentCount!: number; + minArgumentCount!: number; // Undefined is used to indicate the value has not been computed. If, after computing, the // symbol has no doc comment, then the empty array will be returned. @@ -622,28 +622,28 @@ class TypeObject implements Type { this.flags = flags; } - getDeclaration(): SignatureDeclaration { - return this.declaration; - } - getTypeParameters(): TypeParameter[] | undefined { - return this.typeParameters; - } - getParameters(): Symbol[] { - return this.parameters; - } - getReturnType(): Type { - return this.checker.getReturnTypeOfSignature(this); - } - getTypeParameterAtPosition(pos: number): Type { - const type = this.checker.getParameterType(this, pos); - if (type.isIndexType() && isThisTypeParameter(type.type)) { - const constraint = type.type.getConstraint(); - if (constraint) { - return this.checker.getIndexType(constraint); - } + getDeclaration(): SignatureDeclaration { + return this.declaration; + } + getTypeParameters(): TypeParameter[] | undefined { + return this.typeParameters; + } + getParameters(): Symbol[] { + return this.parameters; + } + getReturnType(): Type { + return this.checker.getReturnTypeOfSignature(this); + } + getTypeParameterAtPosition(pos: number): Type { + const type = this.checker.getParameterType(this, pos); + if (type.isIndexType() && isThisTypeParameter(type.type)) { + const constraint = type.type.getConstraint(); + if (constraint) { + return this.checker.getIndexType(constraint); } - return type; } + return type; + } getDocumentationComment(): SymbolDisplayPart[] { return this.documentationComment || (this.documentationComment = getDocumentationComment(singleElementArray(this.declaration), this.checker)); diff --git a/tests/cases/fourslash/fullCaseCompletions3.ts b/tests/cases/fourslash/fullCaseCompletions3.ts index 924e0211f512c..248acab7e1742 100644 --- a/tests/cases/fourslash/fullCaseCompletions3.ts +++ b/tests/cases/fourslash/fullCaseCompletions3.ts @@ -84,11 +84,6 @@ verify.completions( includes: [ exhaustiveCaseCompletion, ], - // exact: [ - // { name: "E", isRecommended: true }, - // "u", - // ...completion.globals, - // ], preferences: { includeCompletionsWithInsertText: true, } @@ -99,7 +94,7 @@ verify.completions( "E", "u", ...completion.globals, - exhaustiveCaseCompletion, // TODO: shouldn't be here but is + exhaustiveCaseCompletion, ], preferences: { includeCompletionsWithInsertText: true, From 599fb305064e1d062e27baf5c09c8e37d5c9969d Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 10 Nov 2022 16:17:35 -0800 Subject: [PATCH 16/23] use appropriate quotes --- src/services/completions.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 326fd26bac7fd..15f00de5fa59c 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -692,6 +692,7 @@ function getExhaustiveCaseSnippets( } 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[]) { @@ -711,7 +712,7 @@ function getExhaustiveCaseSnippets( if (!typeNode) { return undefined; } - const expr = typeNodeToExpression(typeNode, target); + const expr = typeNodeToExpression(typeNode, target, quotePreference); if (!expr) { return undefined; } @@ -842,14 +843,16 @@ function newCaseClauseTracker(checker: TypeChecker): CaseClauseTracker { } } -function typeNodeToExpression(typeNode: TypeNode, languageVersion: ScriptTarget): Expression | undefined { +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); + return entityNameToExpression(typeName, languageVersion, quotePreference); case SyntaxKind.IndexedAccessType: - const objectExpression = typeNodeToExpression((typeNode as IndexedAccessTypeNode).objectType, languageVersion); - const indexExpression = typeNodeToExpression((typeNode as IndexedAccessTypeNode).indexType, languageVersion); + 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); @@ -857,16 +860,16 @@ function typeNodeToExpression(typeNode: TypeNode, languageVersion: ScriptTarget) const literal = (typeNode as LiteralTypeNode).literal; switch (literal.kind) { case SyntaxKind.StringLiteral: - return factory.createStringLiteral(literal.text); // >> TODO: where to get 'issinglequote' from? + 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); + 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); + return entityNameToExpression((typeNode as TypeQueryNode).exprName, languageVersion, quotePreference); case SyntaxKind.ImportType: Debug.fail(`We should not get an import type after calling 'codefix.typeToAutoImportableTypeNode'.`); } @@ -874,16 +877,20 @@ function typeNodeToExpression(typeNode: TypeNode, languageVersion: ScriptTarget) return undefined; } -function entityNameToExpression(entityName: EntityName, languageVersion: ScriptTarget): Expression { +function entityNameToExpression(entityName: EntityName, languageVersion: ScriptTarget, quotePreference: QuotePreference): Expression { if (isIdentifier(entityName)) { return entityName; } const realName = unescapeLeadingUnderscores(entityName.right.escapedText); if (canUsePropertyAccess(realName, languageVersion)) { - return factory.createPropertyAccessExpression(entityNameToExpression(entityName.left, languageVersion), realName); + return factory.createPropertyAccessExpression( + entityNameToExpression(entityName.left, languageVersion, quotePreference), + realName); } else { - return factory.createElementAccessExpression(entityNameToExpression(entityName.left, languageVersion), factory.createStringLiteral(`"${realName}"`)); // >> TODO: this is wrong, use appropriate quotes + return factory.createElementAccessExpression( + entityNameToExpression(entityName.left, languageVersion, quotePreference), + factory.createStringLiteral(realName, quotePreference === QuotePreference.Single)); } } From 97dcf69f33f57268e2e042157edf3f6ee17e3ce5 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 10 Nov 2022 16:20:08 -0800 Subject: [PATCH 17/23] small indentation fix --- src/services/completions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 15f00de5fa59c..50aed4b75a48b 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1119,11 +1119,11 @@ function createCompletionEntry( } } - if (useBraces) { - insertText = `${escapeSnippetText(name)}={$1}`; - isSnippet = true; - } + if (useBraces) { + insertText = `${escapeSnippetText(name)}={$1}`; + isSnippet = true; } + } if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { return undefined; From 3b92638a35f63f7863f1a80ea9890b1676126298 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 14 Nov 2022 11:50:23 -0800 Subject: [PATCH 18/23] refactor case clause tracker --- src/services/completions.ts | 73 +++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 50aed4b75a48b..6af3d4f0e9086 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -58,7 +58,7 @@ import { SymbolFlags, SymbolId, SyntaxKind, TextChange, textChanges, textPart, TextRange, TextSpan, timestamp, Token, TokenSyntaxKind, tokenToString, tryCast, tryGetImportFromModuleSpecifier, Type, TypeChecker, TypeElement, TypeFlags, typeHasCallOrConstructSignatures, TypeLiteralNode, TypeOnlyAliasDeclaration, unescapeLeadingUnderscores, - UnionReduction, UnionType, UserPreferences, VariableDeclaration, walkUpParenthesizedExpressions, CaseBlock, canUsePropertyAccess, CaseClause, endsWith, EntityName, EnumMember, IndexedAccessTypeNode, isCaseBlock, isDefaultClause, isEnumMember, isLiteralExpression, LiteralType, LiteralTypeNode, map, NumericLiteral, ParenthesizedTypeNode, parseBigInt, TypeNode, TypeQueryNode, TypeReferenceNode, + UnionReduction, UnionType, UserPreferences, VariableDeclaration, walkUpParenthesizedExpressions, CaseBlock, canUsePropertyAccess, CaseClause, endsWith, EntityName, EnumMember, IndexedAccessTypeNode, isCaseBlock, isDefaultClause, isEnumMember, isLiteralExpression, LiteralType, LiteralTypeNode, map, NumericLiteral, ParenthesizedTypeNode, parseBigInt, TypeNode, TypeQueryNode, TypeReferenceNode, DefaultClause, } from "./_namespaces/ts"; import { StringCompletions } from "./_namespaces/ts.Completions"; @@ -684,12 +684,7 @@ function getExhaustiveCaseSnippets( 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); - for (const clause of clauses) { - if (!isDefaultClause(clause)) { - tracker.addClause(clause); - } - } + const tracker = newCaseClauseTracker(checker, clauses); const target = getEmitScriptTarget(options); const quotePreference = getQuotePreference(sourceFile, preferences); @@ -775,52 +770,52 @@ function getExhaustiveCaseSnippets( } interface CaseClauseTracker { - addClause(clause: CaseClause): void; addValue(value: string | number): void; hasValue(value: string | number | PseudoBigInt): boolean; } -function newCaseClauseTracker(checker: TypeChecker): CaseClauseTracker { +function newCaseClauseTracker(checker: TypeChecker, clauses: readonly (CaseClause | DefaultClause)[]): CaseClauseTracker { const existingStrings = new Set(); const existingNumbers = new Set(); const existingBigInts = new Set(); - return { - addValue, - addClause, - hasValue, - }; - - function addClause(clause: CaseClause) { - 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; + 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); + 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": From 1894d2e2cd8fe3280a7207b73a2ab0fbee621fe5 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 22 Nov 2022 15:58:15 -0800 Subject: [PATCH 19/23] experiment: support tabstops after each case clause --- src/compiler/utilities.ts | 7 ++++++- src/services/completions.ts | 10 ++++++++-- src/services/services.ts | 5 ++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 3ac7509e9c947..a4ae34bbf9be3 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -90,7 +90,7 @@ import { TypePredicate, TypePredicateKind, TypeReferenceNode, unescapeLeadingUnderscores, UnionOrIntersectionTypeNode, ValidImportTypeNode, VariableDeclaration, VariableDeclarationInitializedTo, VariableDeclarationList, VariableLikeDeclaration, VariableStatement, version, WhileStatement, WithStatement, WriteFileCallback, - WriteFileCallbackData, YieldExpression, ResolutionMode, isIdentifierStart, + WriteFileCallbackData, YieldExpression, ResolutionMode, isIdentifierStart, getSnippetElement, SnippetKind, } from "./_namespaces/ts"; /** @internal */ @@ -8691,3 +8691,8 @@ export function canUsePropertyAccess(name: string, languageVersion: ScriptTarget 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 6af3d4f0e9086..bd69aebcad7c0 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -732,7 +732,12 @@ function getExhaustiveCaseSnippets( return undefined; } - const newClauses = map(elements, element => { + const newClauses = map(elements, (element, i) => { + if (preferences.includeCompletionsWithSnippetText) { + const tabstopStmt = factory.createEmptyStatement(); + setSnippetElement(tabstopStmt, { kind: SnippetKind.TabStop, order: i + 1 }); + return factory.createCaseClause(element, [tabstopStmt]); + } return factory.createCaseClause(element, []); }); const printer = createSnippetPrinter({ @@ -752,7 +757,7 @@ function getExhaustiveCaseSnippets( factory.createNodeArray(newClauses), sourceFile); - const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([first(newClauses)!]), sourceFile); + const firstClause = printer.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([factory.createCaseClause(first(elements), [])]), sourceFile); return { entry: { name: `${firstClause} ...`, @@ -761,6 +766,7 @@ function getExhaustiveCaseSnippets( insertText, hasAction: importAdder.hasFixes() || undefined, source: CompletionSource.SwitchCases, + isSnippet: preferences.includeCompletionsWithSnippetText ? true : undefined, }, importAdder, }; diff --git a/src/services/services.ts b/src/services/services.ts index 703b169d60dc6..1ae520217d2f0 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -54,7 +54,7 @@ import { TextRange, TextSpan, textSpanEnd, timestamp, TodoComment, TodoCommentDescriptor, Token, toPath, tracing, TransformFlags, TransientSymbol, Type, TypeChecker, TypeFlags, TypeNode, TypeParameter, TypePredicate, TypeReference, typeToDisplayParts, UnderscoreEscapedMap, UnionOrIntersectionType, UnionType, updateSourceFile, - UserPreferences, VariableDeclaration, + UserPreferences, VariableDeclaration, hasTabstop, } from "./_namespaces/ts"; /** The version of the language service API */ @@ -234,6 +234,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)); From 90767fc13c68391df02b2ab727f839d77b0aaaa9 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 23 Nov 2022 14:41:20 -0800 Subject: [PATCH 20/23] address small CR comments --- src/compiler/utilities.ts | 3 +++ src/services/completions.ts | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index a4ae34bbf9be3..8996a598ba81c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -8686,6 +8686,9 @@ export function isOptionalJSDocPropertyLikeTag(node: Node): node is JSDocPropert /** @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) : diff --git a/src/services/completions.ts b/src/services/completions.ts index bd69aebcad7c0..8c03b07a34ae2 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -882,16 +882,16 @@ function entityNameToExpression(entityName: EntityName, languageVersion: ScriptT if (isIdentifier(entityName)) { return entityName; } - const realName = unescapeLeadingUnderscores(entityName.right.escapedText); - if (canUsePropertyAccess(realName, languageVersion)) { + const unescapedName = unescapeLeadingUnderscores(entityName.right.escapedText); + if (canUsePropertyAccess(unescapedName, languageVersion)) { return factory.createPropertyAccessExpression( entityNameToExpression(entityName.left, languageVersion, quotePreference), - realName); + unescapedName); } else { return factory.createElementAccessExpression( entityNameToExpression(entityName.left, languageVersion, quotePreference), - factory.createStringLiteral(realName, quotePreference === QuotePreference.Single)); + factory.createStringLiteral(unescapedName, quotePreference === QuotePreference.Single)); } } From d1c89687fe48a8cd32878179630e1b78d360f7cd Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 30 Nov 2022 14:24:57 -0800 Subject: [PATCH 21/23] fix completion entry details; add test case --- src/services/completions.ts | 23 +++++++++++++------ tests/cases/fourslash/fullCaseCompletions1.ts | 20 ++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 8c03b07a34ae2..d777e2800b9ed 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -2008,19 +2008,28 @@ export function getCompletionEntryDetails( host, program, /*formatContext*/ undefined)!; - const changes = textChanges.ChangeTracker.with( - { host, formatContext, preferences }, - importAdder.writeFixes); + 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, - codeActions: [{ - changes, - description: diagnosticToString([Diagnostics.Includes_imports_of_types_referenced_by_0, name]), - }], }; } case "none": diff --git a/tests/cases/fourslash/fullCaseCompletions1.ts b/tests/cases/fourslash/fullCaseCompletions1.ts index fedd8ccf50420..60f47c6ef8c0c 100644 --- a/tests/cases/fourslash/fullCaseCompletions1.ts +++ b/tests/cases/fourslash/fullCaseCompletions1.ts @@ -84,4 +84,24 @@ case F.F:`, 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 From 3980b9345236745f025ba0e0f6969d661ff94430 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 1 Dec 2022 10:43:57 -0800 Subject: [PATCH 22/23] fix lint errors --- src/compiler/checker.ts | 10 ++++----- src/compiler/utilities.ts | 8 +++---- src/services/completions.ts | 42 ++++++++++++++++++------------------- src/services/services.ts | 4 ++-- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 536d47bd56c84..8617a965feccd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -52,6 +52,7 @@ import { canHaveIllegalDecorators, canHaveIllegalModifiers, canHaveModifiers, + canUsePropertyAccess, cartesianProduct, CaseBlock, CaseClause, @@ -336,8 +337,8 @@ import { hasAccessorModifier, hasAmbientModifier, hasContextSensitiveParameters, - hasDecorators, HasDecorators, + hasDecorators, hasDynamicName, hasEffectiveModifier, hasEffectiveModifiers, @@ -346,8 +347,8 @@ import { hasExtension, HasIllegalDecorators, HasIllegalModifiers, - hasInitializer, HasInitializer, + hasInitializer, hasJSDocNodes, hasJSDocParameterTags, hasJsonModuleEmitEnabled, @@ -675,6 +676,7 @@ import { isTypeReferenceNode, isTypeReferenceType, isUMDExportSymbol, + isValidBigIntString, isValidESSymbolDeclaration, isValidTypeOnlyAliasUseSite, isValueSignatureDeclaration, @@ -824,6 +826,7 @@ import { parseIsolatedEntityName, parseNodeFactory, parsePseudoBigInt, + parseValidBigInt, Path, pathIsRelative, PatternAmbientModule, @@ -1008,9 +1011,6 @@ import { WideningContext, WithStatement, YieldExpression, - canUsePropertyAccess, - isValidBigIntString, - parseValidBigInt, } from "./_namespaces/ts"; import * as performance from "./_namespaces/ts.performance"; import * as moduleSpecifiers from "./_namespaces/ts.moduleSpecifiers"; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 7d44d5f13360e..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, @@ -515,9 +518,6 @@ import { WriteFileCallback, WriteFileCallbackData, YieldExpression, - getSnippetElement, - isIdentifierStart, - SnippetKind, } from "./_namespaces/ts"; /** @internal */ diff --git a/src/services/completions.ts b/src/services/completions.ts index a863c8ab51506..b098a3507d574 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,34 +342,16 @@ import { TypeFlags, typeHasCallOrConstructSignatures, TypeLiteralNode, + TypeNode, TypeOnlyAliasDeclaration, + TypeQueryNode, + TypeReferenceNode, unescapeLeadingUnderscores, UnionReduction, UnionType, UserPreferences, VariableDeclaration, walkUpParenthesizedExpressions, - canUsePropertyAccess, - CaseBlock, - CaseClause, - DefaultClause, - endsWith, - EntityName, - EnumMember, - IndexedAccessTypeNode, - isCaseBlock, - isDefaultClause, - isEnumMember, - isLiteralExpression, - LiteralType, - LiteralTypeNode, - map, - NumericLiteral, - ParenthesizedTypeNode, - parseBigInt, - TypeNode, - TypeQueryNode, - TypeReferenceNode, } from "./_namespaces/ts"; import { StringCompletions } from "./_namespaces/ts.Completions"; diff --git a/src/services/services.ts b/src/services/services.ts index 4b9c3c9fcf214..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, @@ -318,7 +319,6 @@ import { updateSourceFile, UserPreferences, VariableDeclaration, - hasTabstop, } from "./_namespaces/ts"; /** The version of the language service API */ From 88231087f4e718e13647ea3bc5ecefc1197e3f61 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Thu, 1 Dec 2022 13:22:14 -0800 Subject: [PATCH 23/23] remove space before tab stops; refactor --- src/services/completions.ts | 81 ++++++++++++++----- ...ions1.ts => exhaustiveCaseCompletions1.ts} | 6 +- ...ions2.ts => exhaustiveCaseCompletions2.ts} | 0 ...ions3.ts => exhaustiveCaseCompletions3.ts} | 0 ...ions4.ts => exhaustiveCaseCompletions4.ts} | 0 ...ions5.ts => exhaustiveCaseCompletions5.ts} | 0 6 files changed, 63 insertions(+), 24 deletions(-) rename tests/cases/fourslash/{fullCaseCompletions1.ts => exhaustiveCaseCompletions1.ts} (94%) rename tests/cases/fourslash/{fullCaseCompletions2.ts => exhaustiveCaseCompletions2.ts} (100%) rename tests/cases/fourslash/{fullCaseCompletions3.ts => exhaustiveCaseCompletions3.ts} (100%) rename tests/cases/fourslash/{fullCaseCompletions4.ts => exhaustiveCaseCompletions4.ts} (100%) rename tests/cases/fourslash/{fullCaseCompletions5.ts => exhaustiveCaseCompletions5.ts} (100%) diff --git a/src/services/completions.ts b/src/services/completions.ts index b098a3507d574..a09aebcb7d1a3 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1025,32 +1025,25 @@ function getExhaustiveCaseSnippets( return undefined; } - const newClauses = map(elements, (element, i) => { - if (preferences.includeCompletionsWithSnippetText) { - const tabstopStmt = factory.createEmptyStatement(); - setSnippetElement(tabstopStmt, { kind: SnippetKind.TabStop, order: i + 1 }); - return factory.createCaseClause(element, [tabstopStmt]); - } - return factory.createCaseClause(element, []); - }); + 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(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), + newLine: getNewLineKind(newLineChar), }); - const insertText = formatContext - ? printer.printAndFormatSnippetList( - ListFormat.MultiLine | ListFormat.NoTrailingNewLine, - factory.createNodeArray(newClauses), - sourceFile, - formatContext) - : printer.printSnippetList( - ListFormat.MultiLine | ListFormat.NoTrailingNewLine, - factory.createNodeArray(newClauses), - sourceFile); + 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.printSnippetList(ListFormat.SingleLine, factory.createNodeArray([factory.createCaseClause(first(elements), [])]), sourceFile); + const firstClause = printer.printNode(EmitHint.Unspecified, newClauses[0], sourceFile); return { entry: { name: `${firstClause} ...`, @@ -1821,6 +1814,8 @@ function createSnippetPrinter( return { printSnippetList, printAndFormatSnippetList, + printNode, + printAndFormatNode, }; // The formatter/scanner will have issues with snippet-escaped text, @@ -1840,7 +1835,7 @@ function createSnippetPrinter( } } - /* Snippet-escaping version of `printer.printList`. */ + /** Snippet-escaping version of `printer.printList`. */ function printSnippetList( format: ListFormat, list: NodeArray, @@ -1894,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 { diff --git a/tests/cases/fourslash/fullCaseCompletions1.ts b/tests/cases/fourslash/exhaustiveCaseCompletions1.ts similarity index 94% rename from tests/cases/fourslash/fullCaseCompletions1.ts rename to tests/cases/fourslash/exhaustiveCaseCompletions1.ts index 60f47c6ef8c0c..a0bef292f1b34 100644 --- a/tests/cases/fourslash/fullCaseCompletions1.ts +++ b/tests/cases/fourslash/exhaustiveCaseCompletions1.ts @@ -94,9 +94,9 @@ case F.F:`, sortText: completion.SortText.GlobalsOrKeywords, isSnippet: true, insertText: -`case F.D: $1 -case F.E: $2 -case F.F: $3`, +`case F.D:$1 +case F.E:$2 +case F.F:$3`, }, ], preferences: { diff --git a/tests/cases/fourslash/fullCaseCompletions2.ts b/tests/cases/fourslash/exhaustiveCaseCompletions2.ts similarity index 100% rename from tests/cases/fourslash/fullCaseCompletions2.ts rename to tests/cases/fourslash/exhaustiveCaseCompletions2.ts diff --git a/tests/cases/fourslash/fullCaseCompletions3.ts b/tests/cases/fourslash/exhaustiveCaseCompletions3.ts similarity index 100% rename from tests/cases/fourslash/fullCaseCompletions3.ts rename to tests/cases/fourslash/exhaustiveCaseCompletions3.ts diff --git a/tests/cases/fourslash/fullCaseCompletions4.ts b/tests/cases/fourslash/exhaustiveCaseCompletions4.ts similarity index 100% rename from tests/cases/fourslash/fullCaseCompletions4.ts rename to tests/cases/fourslash/exhaustiveCaseCompletions4.ts diff --git a/tests/cases/fourslash/fullCaseCompletions5.ts b/tests/cases/fourslash/exhaustiveCaseCompletions5.ts similarity index 100% rename from tests/cases/fourslash/fullCaseCompletions5.ts rename to tests/cases/fourslash/exhaustiveCaseCompletions5.ts