From 4cc069698153bfafa85b1365c068536e4902205e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=96=87=E7=92=90?= Date: Wed, 26 Jun 2019 17:36:21 +0800 Subject: [PATCH 1/5] add completion for promise context --- src/services/completions.ts | 58 ++++++++++++++----- .../fourslash/completionOfAwaitPromise1.ts | 17 ++++++ .../fourslash/completionOfAwaitPromise2.ts | 18 ++++++ .../fourslash/completionOfAwaitPromise3.ts | 18 ++++++ .../fourslash/completionOfAwaitPromise4.ts | 15 +++++ .../fourslash/completionOfAwaitPromise5.ts | 18 ++++++ 6 files changed, 131 insertions(+), 13 deletions(-) create mode 100644 tests/cases/fourslash/completionOfAwaitPromise1.ts create mode 100644 tests/cases/fourslash/completionOfAwaitPromise2.ts create mode 100644 tests/cases/fourslash/completionOfAwaitPromise3.ts create mode 100644 tests/cases/fourslash/completionOfAwaitPromise4.ts create mode 100644 tests/cases/fourslash/completionOfAwaitPromise5.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 6d1d48b22b828..79e93525094b7 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. @@ -235,6 +238,7 @@ namespace ts.Completions { typeChecker: TypeChecker, name: string, needsConvertPropertyAccess: boolean, + needsConvertAwait: boolean | undefined, origin: SymbolOriginInfo | undefined, recommendedCompletion: Symbol | undefined, propertyAccessToConvert: PropertyAccessExpression | undefined, @@ -263,6 +267,12 @@ namespace ts.Completions { replacementSpan = createTextSpanFromNode(isJsxInitializer, sourceFile); } } + if (needsConvertAwait && 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 +322,7 @@ namespace ts.Completions { log: Log, kind: CompletionKind, preferences: UserPreferences, - propertyAccessToConvert?: PropertyAccessExpression | undefined, + propertyAccessToConvert?: PropertyAccessExpression, isJsxInitializer?: IsJsxInitializer, recommendedCompletion?: Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, @@ -330,7 +340,7 @@ namespace ts.Completions { if (!info) { continue; } - const { name, needsConvertPropertyAccess } = info; + const { name, needsConvertPropertyAccess, needsConvertAwait } = info; if (uniques.has(name)) { continue; } @@ -343,6 +353,7 @@ namespace ts.Completions { typeChecker, name, needsConvertPropertyAccess, + needsConvertAwait, origin, recommendedCompletion, propertyAccessToConvert, @@ -981,7 +992,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 +1007,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 +1025,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) { + 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,10 +1060,17 @@ namespace ts.Completions { !moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false }; } else if (preferences.includeCompletionsWithInsertText) { - symbols.push(symbol); + addSymbol(symbol); } } else { + addSymbol(symbol); + } + + function addSymbol (symbol: Symbol) { + if (insertAwait && !symbolToOriginInfoMap[getSymbolId(symbol)]) { + symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Promise }; + } symbols.push(symbol); } } @@ -1978,6 +2008,7 @@ namespace ts.Completions { interface CompletionEntryDisplayNameForSymbol { readonly name: string; readonly needsConvertPropertyAccess: boolean; + readonly needsConvertAwait: boolean; } function getCompletionEntryDisplayNameForSymbol( symbol: Symbol, @@ -1985,6 +2016,7 @@ namespace ts.Completions { origin: SymbolOriginInfo | undefined, kind: CompletionKind, ): CompletionEntryDisplayNameForSymbol | undefined { + const needsConvertAwait = !!(origin && originIsPromise(origin)); const name = getSymbolName(symbol, origin, target); if (name === undefined // If the symbol is external module, don't show it in the completion list @@ -1995,18 +2027,18 @@ namespace ts.Completions { return undefined; } - const validIdentifierResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false }; + const validIdentifierResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false, needsConvertAwait }; if (isIdentifierText(name, target)) return validIdentifierResult; switch (kind) { case CompletionKind.MemberLike: return undefined; case CompletionKind.ObjectPropertyDeclaration: // TODO: GH#18169 - return { name: JSON.stringify(name), needsConvertPropertyAccess: false }; + return { name: JSON.stringify(name), needsConvertPropertyAccess: false, needsConvertAwait }; case CompletionKind.PropertyAccess: case CompletionKind.Global: // For a 'this.' completion it will be in a global context, but may have a non-identifier name. // Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547 - return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true }; + return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true, needsConvertAwait }; case CompletionKind.None: case CompletionKind.String: return validIdentifierResult; 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, + }, +}); From 1c2bb1c7d0870954ebeaac074c81dcef4ea349b5 Mon Sep 17 00:00:00 2001 From: kingwl Date: Thu, 11 Jul 2019 00:55:57 +0800 Subject: [PATCH 2/5] check insert text inside add symbol helper --- src/services/completions.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 79e93525094b7..29722d52078bf 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1059,7 +1059,7 @@ namespace ts.Completions { symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = !moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false }; } - else if (preferences.includeCompletionsWithInsertText) { + else { addSymbol(symbol); } } @@ -1068,10 +1068,12 @@ namespace ts.Completions { } function addSymbol (symbol: Symbol) { - if (insertAwait && !symbolToOriginInfoMap[getSymbolId(symbol)]) { - symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Promise }; + if (preferences.includeCompletionsWithInsertText) { + if (insertAwait && !symbolToOriginInfoMap[getSymbolId(symbol)]) { + symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Promise }; + } + symbols.push(symbol); } - symbols.push(symbol); } } From 9cbac995d1c531e6e162434771f667b065c40fb9 Mon Sep 17 00:00:00 2001 From: kingwl Date: Thu, 11 Jul 2019 01:22:54 +0800 Subject: [PATCH 3/5] fix incorrect branch --- src/services/completions.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 29722d52078bf..1cc880c58411e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1059,21 +1059,19 @@ namespace ts.Completions { symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = !moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false }; } - else { - addSymbol(symbol); + else if(preferences.includeCompletionsWithInsertText) { + addPromiseSymbolOriginInfo(symbol); } } else { - addSymbol(symbol); + addPromiseSymbolOriginInfo(symbol); } - function addSymbol (symbol: Symbol) { - if (preferences.includeCompletionsWithInsertText) { - if (insertAwait && !symbolToOriginInfoMap[getSymbolId(symbol)]) { - symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Promise }; - } - symbols.push(symbol); + function addPromiseSymbolOriginInfo (symbol: Symbol) { + if (insertAwait && preferences.includeCompletionsWithInsertText && !symbolToOriginInfoMap[getSymbolId(symbol)]) { + symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Promise }; } + symbols.push(symbol); } } From e4e7aba2e68f19350a23f382be37c514d28fe10f Mon Sep 17 00:00:00 2001 From: kingwl Date: Wed, 17 Jul 2019 13:50:10 +0800 Subject: [PATCH 4/5] avoid completions with includeCompletionsWithInsertText perferences --- src/services/completions.ts | 7 ++++--- .../fourslash/completionOfAwaitPromise6.ts | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/cases/fourslash/completionOfAwaitPromise6.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index 1cc880c58411e..e84f6173f888b 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1031,7 +1031,7 @@ namespace ts.Completions { } } - if (insertAwait) { + if (insertAwait && preferences.includeCompletionsWithInsertText) { const promiseType = typeChecker.getPromisedTypeOfPromise(type); if (promiseType) { for (const symbol of promiseType.getApparentProperties()) { @@ -1059,19 +1059,20 @@ namespace ts.Completions { symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = !moduleSymbol || !isExternalModuleSymbol(moduleSymbol) ? { kind: SymbolOriginInfoKind.SymbolMemberNoExport } : { kind: SymbolOriginInfoKind.SymbolMemberExport, moduleSymbol, isDefaultExport: false }; } - else if(preferences.includeCompletionsWithInsertText) { + 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 }; } - symbols.push(symbol); } } 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, + }, +}); From f527388021867905e2e05bbcab04e5871121a579 Mon Sep 17 00:00:00 2001 From: kingwl Date: Thu, 18 Jul 2019 13:07:37 +0800 Subject: [PATCH 5/5] avoid useless parameter --- src/services/completions.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index e84f6173f888b..dd71bde17fc1d 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -238,7 +238,6 @@ namespace ts.Completions { typeChecker: TypeChecker, name: string, needsConvertPropertyAccess: boolean, - needsConvertAwait: boolean | undefined, origin: SymbolOriginInfo | undefined, recommendedCompletion: Symbol | undefined, propertyAccessToConvert: PropertyAccessExpression | undefined, @@ -267,7 +266,7 @@ namespace ts.Completions { replacementSpan = createTextSpanFromNode(isJsxInitializer, sourceFile); } } - if (needsConvertAwait && propertyAccessToConvert) { + if (origin && originIsPromise(origin) && propertyAccessToConvert) { if (insertText === undefined) insertText = name; const awaitText = `(await ${propertyAccessToConvert.expression.getText()})`; insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}.${insertText}`; @@ -340,7 +339,7 @@ namespace ts.Completions { if (!info) { continue; } - const { name, needsConvertPropertyAccess, needsConvertAwait } = info; + const { name, needsConvertPropertyAccess } = info; if (uniques.has(name)) { continue; } @@ -353,7 +352,6 @@ namespace ts.Completions { typeChecker, name, needsConvertPropertyAccess, - needsConvertAwait, origin, recommendedCompletion, propertyAccessToConvert, @@ -2009,7 +2007,6 @@ namespace ts.Completions { interface CompletionEntryDisplayNameForSymbol { readonly name: string; readonly needsConvertPropertyAccess: boolean; - readonly needsConvertAwait: boolean; } function getCompletionEntryDisplayNameForSymbol( symbol: Symbol, @@ -2017,7 +2014,6 @@ namespace ts.Completions { origin: SymbolOriginInfo | undefined, kind: CompletionKind, ): CompletionEntryDisplayNameForSymbol | undefined { - const needsConvertAwait = !!(origin && originIsPromise(origin)); const name = getSymbolName(symbol, origin, target); if (name === undefined // If the symbol is external module, don't show it in the completion list @@ -2028,18 +2024,18 @@ namespace ts.Completions { return undefined; } - const validIdentifierResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false, needsConvertAwait }; + const validIdentifierResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false }; if (isIdentifierText(name, target)) return validIdentifierResult; switch (kind) { case CompletionKind.MemberLike: return undefined; case CompletionKind.ObjectPropertyDeclaration: // TODO: GH#18169 - return { name: JSON.stringify(name), needsConvertPropertyAccess: false, needsConvertAwait }; + return { name: JSON.stringify(name), needsConvertPropertyAccess: false }; case CompletionKind.PropertyAccess: case CompletionKind.Global: // For a 'this.' completion it will be in a global context, but may have a non-identifier name. // Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547 - return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true, needsConvertAwait }; + return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true }; case CompletionKind.None: case CompletionKind.String: return validIdentifierResult;