diff --git a/src/services/completions.ts b/src/services/completions.ts index 6d1d48b22b828..dd71bde17fc1d 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -9,8 +9,8 @@ namespace ts.Completions { } export type Log = (message: string) => void; - const enum SymbolOriginInfoKind { ThisType, SymbolMemberNoExport, SymbolMemberExport, Export } - type SymbolOriginInfo = { kind: SymbolOriginInfoKind.ThisType } | { kind: SymbolOriginInfoKind.SymbolMemberNoExport } | SymbolOriginInfoExport; + const enum SymbolOriginInfoKind { ThisType, SymbolMemberNoExport, SymbolMemberExport, Export, Promise } + type SymbolOriginInfo = { kind: SymbolOriginInfoKind.ThisType } | { kind: SymbolOriginInfoKind.Promise } | { kind: SymbolOriginInfoKind.SymbolMemberNoExport } | SymbolOriginInfoExport; interface SymbolOriginInfoExport { kind: SymbolOriginInfoKind.SymbolMemberExport | SymbolOriginInfoKind.Export; moduleSymbol: Symbol; @@ -22,6 +22,9 @@ namespace ts.Completions { function originIsExport(origin: SymbolOriginInfo): origin is SymbolOriginInfoExport { return origin.kind === SymbolOriginInfoKind.SymbolMemberExport || origin.kind === SymbolOriginInfoKind.Export; } + function originIsPromise(origin: SymbolOriginInfo): boolean { + return origin.kind === SymbolOriginInfoKind.Promise; + } /** * Map from symbol id -> SymbolOriginInfo. @@ -263,6 +266,12 @@ namespace ts.Completions { replacementSpan = createTextSpanFromNode(isJsxInitializer, sourceFile); } } + if (origin && originIsPromise(origin) && propertyAccessToConvert) { + if (insertText === undefined) insertText = name; + const awaitText = `(await ${propertyAccessToConvert.expression.getText()})`; + insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}.${insertText}`; + replacementSpan = createTextSpanFromBounds(propertyAccessToConvert.getStart(sourceFile), propertyAccessToConvert.end); + } if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { return undefined; @@ -312,7 +321,7 @@ namespace ts.Completions { log: Log, kind: CompletionKind, preferences: UserPreferences, - propertyAccessToConvert?: PropertyAccessExpression | undefined, + propertyAccessToConvert?: PropertyAccessExpression, isJsxInitializer?: IsJsxInitializer, recommendedCompletion?: Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, @@ -981,7 +990,7 @@ namespace ts.Completions { if (!isTypeLocation && symbol.declarations && symbol.declarations.some(d => d.kind !== SyntaxKind.SourceFile && d.kind !== SyntaxKind.ModuleDeclaration && d.kind !== SyntaxKind.EnumDeclaration)) { - addTypeProperties(typeChecker.getTypeOfSymbolAtLocation(symbol, node)); + addTypeProperties(typeChecker.getTypeOfSymbolAtLocation(symbol, node), !!(node.flags & NodeFlags.AwaitContext)); } return; @@ -996,13 +1005,14 @@ namespace ts.Completions { } if (!isTypeLocation) { - addTypeProperties(typeChecker.getTypeAtLocation(node)); + addTypeProperties(typeChecker.getTypeAtLocation(node), !!(node.flags & NodeFlags.AwaitContext)); } } - function addTypeProperties(type: Type): void { + function addTypeProperties(type: Type, insertAwait?: boolean): void { isNewIdentifierLocation = !!type.getStringIndexType(); + const propertyAccess = node.kind === SyntaxKind.ImportType ? node : node.parent; if (isUncheckedFile) { // In javascript files, for union types, we don't just get the members that // the individual types have in common, we also include all the members that @@ -1013,14 +1023,25 @@ namespace ts.Completions { } else { for (const symbol of type.getApparentProperties()) { - if (typeChecker.isValidPropertyAccessForCompletions(node.kind === SyntaxKind.ImportType ? node : node.parent, type, symbol)) { + if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, symbol)) { addPropertySymbol(symbol); } } } + + if (insertAwait && preferences.includeCompletionsWithInsertText) { + const promiseType = typeChecker.getPromisedTypeOfPromise(type); + if (promiseType) { + for (const symbol of promiseType.getApparentProperties()) { + if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, promiseType, symbol)) { + addPropertySymbol(symbol, /* insertAwait */ true); + } + } + } + } } - function addPropertySymbol(symbol: Symbol) { + function addPropertySymbol(symbol: Symbol, insertAwait?: boolean) { // For a computed property with an accessible name like `Symbol.iterator`, // we'll add a completion for the *name* `Symbol` instead of for the property. // If this is e.g. [Symbol.iterator], add a completion for `Symbol`. @@ -1037,12 +1058,20 @@ namespace ts.Completions { !moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false }; } else if (preferences.includeCompletionsWithInsertText) { + addPromiseSymbolOriginInfo(symbol); symbols.push(symbol); } } else { + addPromiseSymbolOriginInfo(symbol); symbols.push(symbol); } + + function addPromiseSymbolOriginInfo (symbol: Symbol) { + if (insertAwait && preferences.includeCompletionsWithInsertText && !symbolToOriginInfoMap[getSymbolId(symbol)]) { + symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Promise }; + } + } } /** Given 'a.b.c', returns 'a'. */ diff --git a/tests/cases/fourslash/completionOfAwaitPromise1.ts b/tests/cases/fourslash/completionOfAwaitPromise1.ts new file mode 100644 index 0000000000000..0c72b1202118d --- /dev/null +++ b/tests/cases/fourslash/completionOfAwaitPromise1.ts @@ -0,0 +1,17 @@ +/// + +//// async function foo(x: Promise) { +//// [|x./**/|] +//// } + +const replacementSpan = test.ranges()[0] +verify.completions({ + marker: "", + includes: [ + "then", + { name: "trim", insertText: '(await x).trim', replacementSpan }, + ], + preferences: { + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionOfAwaitPromise2.ts b/tests/cases/fourslash/completionOfAwaitPromise2.ts new file mode 100644 index 0000000000000..fab4ff9020a52 --- /dev/null +++ b/tests/cases/fourslash/completionOfAwaitPromise2.ts @@ -0,0 +1,18 @@ +/// + +//// interface Foo { foo: string } +//// async function foo(x: Promise) { +//// [|x./**/|] +//// } + +const replacementSpan = test.ranges()[0] +verify.completions({ + marker: "", + includes: [ + "then", + { name: "foo", insertText: '(await x).foo', replacementSpan }, + ], + preferences: { + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionOfAwaitPromise3.ts b/tests/cases/fourslash/completionOfAwaitPromise3.ts new file mode 100644 index 0000000000000..aec39eec6ac64 --- /dev/null +++ b/tests/cases/fourslash/completionOfAwaitPromise3.ts @@ -0,0 +1,18 @@ +/// + +//// interface Foo { ["foo-foo"]: string } +//// async function foo(x: Promise) { +//// [|x./**/|] +//// } + +const replacementSpan = test.ranges()[0] +verify.completions({ + marker: "", + includes: [ + "then", + { name: "foo-foo", insertText: '(await x)["foo-foo"]', replacementSpan, }, + ], + preferences: { + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionOfAwaitPromise4.ts b/tests/cases/fourslash/completionOfAwaitPromise4.ts new file mode 100644 index 0000000000000..702efd7090eff --- /dev/null +++ b/tests/cases/fourslash/completionOfAwaitPromise4.ts @@ -0,0 +1,15 @@ +/// + +//// function foo(x: Promise) { +//// [|x./**/|] +//// } + +const replacementSpan = test.ranges()[0] +verify.completions({ + marker: "", + includes: ["then"], + excludes: ["trim"], + preferences: { + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionOfAwaitPromise5.ts b/tests/cases/fourslash/completionOfAwaitPromise5.ts new file mode 100644 index 0000000000000..87a5523fe23c4 --- /dev/null +++ b/tests/cases/fourslash/completionOfAwaitPromise5.ts @@ -0,0 +1,18 @@ +/// + +//// interface Foo { foo: string } +//// async function foo(x: (a: number) => Promise) { +//// [|x(1)./**/|] +//// } + +const replacementSpan = test.ranges()[0] +verify.completions({ + marker: "", + includes: [ + "then", + { name: "foo", insertText: '(await x(1)).foo', replacementSpan }, + ], + preferences: { + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionOfAwaitPromise6.ts b/tests/cases/fourslash/completionOfAwaitPromise6.ts new file mode 100644 index 0000000000000..c09cffdca09be --- /dev/null +++ b/tests/cases/fourslash/completionOfAwaitPromise6.ts @@ -0,0 +1,17 @@ +/// + +//// async function foo(x: Promise) { +//// [|x./**/|] +//// } + +const replacementSpan = test.ranges()[0] +verify.completions({ + marker: "", + exact: [ + "then", + "catch" + ], + preferences: { + includeInsertTextCompletions: false, + }, +});