Skip to content

Commit fd4d8ab

Browse files
author
Andy
authored
Support a 'recommended' completion entry (#20020)
* Support a 'recommended' completion entry * Code review * Restore duplicate comments
1 parent 973cb76 commit fd4d8ab

17 files changed

+235
-62
lines changed

src/compiler/checker.ts

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ namespace ts {
256256
return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false);
257257
},
258258
getJsxNamespace: () => unescapeLeadingUnderscores(getJsxNamespace()),
259+
getAccessibleSymbolChain,
259260
};
260261

261262
const tupleTypes: GenericType[] = [];
@@ -763,10 +764,6 @@ namespace ts {
763764
return nodeLinks[nodeId] || (nodeLinks[nodeId] = { flags: 0 });
764765
}
765766

766-
function getObjectFlags(type: Type): ObjectFlags {
767-
return type.flags & TypeFlags.Object ? (<ObjectType>type).objectFlags : 0;
768-
}
769-
770767
function isGlobalSourceFile(node: Node) {
771768
return node.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule(<SourceFile>node);
772769
}
@@ -10452,20 +10449,6 @@ namespace ts {
1045210449
!hasBaseType(checkClass, getDeclaringClass(p)) : false) ? undefined : checkClass;
1045310450
}
1045410451

10455-
// Return true if the given type is the constructor type for an abstract class
10456-
function isAbstractConstructorType(type: Type) {
10457-
if (getObjectFlags(type) & ObjectFlags.Anonymous) {
10458-
const symbol = type.symbol;
10459-
if (symbol && symbol.flags & SymbolFlags.Class) {
10460-
const declaration = getClassLikeDeclarationOfSymbol(symbol);
10461-
if (declaration && hasModifier(declaration, ModifierFlags.Abstract)) {
10462-
return true;
10463-
}
10464-
}
10465-
}
10466-
return false;
10467-
}
10468-
1046910452
// Return true if the given type is deeply nested. We consider this to be the case when structural type comparisons
1047010453
// for 5 or more occurrences or instantiations of the type have been recorded on the given stack. It is possible,
1047110454
// though highly unlikely, for this test to be true in a situation where a chain of instantiations is not infinitely
@@ -13767,7 +13750,7 @@ namespace ts {
1376713750
// the contextual type of an initializer expression is the type annotation of the containing declaration, if present.
1376813751
function getContextualTypeForInitializerExpression(node: Expression): Type {
1376913752
const declaration = <VariableLikeDeclaration>node.parent;
13770-
if (node === declaration.initializer) {
13753+
if (node === declaration.initializer || node.kind === SyntaxKind.EqualsToken) {
1377113754
const typeNode = getEffectiveTypeAnnotationNode(declaration);
1377213755
if (typeNode) {
1377313756
return getTypeFromTypeNode(typeNode);
@@ -13899,6 +13882,12 @@ namespace ts {
1389913882
case SyntaxKind.AmpersandAmpersandToken:
1390013883
case SyntaxKind.CommaToken:
1390113884
return node === right ? getContextualType(binaryExpression) : undefined;
13885+
case SyntaxKind.EqualsEqualsEqualsToken:
13886+
case SyntaxKind.EqualsEqualsToken:
13887+
case SyntaxKind.ExclamationEqualsEqualsToken:
13888+
case SyntaxKind.ExclamationEqualsToken:
13889+
// For completions after `x === `
13890+
return node === operatorToken ? getTypeOfExpression(binaryExpression.left) : undefined;
1390213891
default:
1390313892
return undefined;
1390413893
}
@@ -14114,9 +14103,13 @@ namespace ts {
1411414103
return getContextualTypeForReturnExpression(node);
1411514104
case SyntaxKind.YieldExpression:
1411614105
return getContextualTypeForYieldOperand(<YieldExpression>parent);
14117-
case SyntaxKind.CallExpression:
1411814106
case SyntaxKind.NewExpression:
14119-
return getContextualTypeForArgument(<CallExpression>parent, node);
14107+
if (node.kind === SyntaxKind.NewKeyword) { // for completions after `new `
14108+
return getContextualType(parent as NewExpression);
14109+
}
14110+
// falls through
14111+
case SyntaxKind.CallExpression:
14112+
return getContextualTypeForArgument(<CallExpression | NewExpression>parent, node);
1412014113
case SyntaxKind.TypeAssertionExpression:
1412114114
case SyntaxKind.AsExpression:
1412214115
return getTypeFromTypeNode((<AssertionExpression>parent).type);
@@ -14150,6 +14143,12 @@ namespace ts {
1415014143
case SyntaxKind.JsxOpeningElement:
1415114144
case SyntaxKind.JsxSelfClosingElement:
1415214145
return getAttributesTypeFromJsxOpeningLikeElement(<JsxOpeningLikeElement>parent);
14146+
case SyntaxKind.CaseClause: {
14147+
if (node.kind === SyntaxKind.CaseKeyword) { // for completions after `case `
14148+
const switchStatement = (parent as CaseClause).parent.parent;
14149+
return getTypeOfExpression(switchStatement.expression);
14150+
}
14151+
}
1415314152
}
1415414153
return undefined;
1415514154
}
@@ -22578,10 +22577,6 @@ namespace ts {
2257822577
return getCheckFlags(s) & CheckFlags.Instantiated ? (<TransientSymbol>s).target : s;
2257922578
}
2258022579

22581-
function getClassLikeDeclarationOfSymbol(symbol: Symbol): Declaration {
22582-
return forEach(symbol.declarations, d => isClassLike(d) ? d : undefined);
22583-
}
22584-
2258522580
function getClassOrInterfaceDeclarationsOfSymbol(symbol: Symbol) {
2258622581
return filter(symbol.declarations, (d: Declaration): d is ClassDeclaration | InterfaceDeclaration =>
2258722582
d.kind === SyntaxKind.ClassDeclaration || d.kind === SyntaxKind.InterfaceDeclaration);

src/compiler/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2818,6 +2818,17 @@ namespace ts {
28182818
/* @internal */ getAllPossiblePropertiesOfTypes(type: ReadonlyArray<Type>): Symbol[];
28192819
/* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol | undefined;
28202820
/* @internal */ getJsxNamespace(): string;
2821+
2822+
/**
2823+
* Note that this will return undefined in the following case:
2824+
* // a.ts
2825+
* export namespace N { export class C { } }
2826+
* // b.ts
2827+
* <<enclosingDeclaration>>
2828+
* Where `C` is the symbol we're looking for.
2829+
* This should be called in a loop climbing parents of the symbol, so we'll get `N`.
2830+
*/
2831+
/* @internal */ getAccessibleSymbolChain(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean): Symbol[] | undefined;
28212832
}
28222833

28232834
export enum NodeBuilderFlags {

src/compiler/utilities.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,6 @@ namespace ts {
465465
return isExternalModule(node) || compilerOptions.isolatedModules || ((getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS) && !!node.commonJsModuleIndicator);
466466
}
467467

468-
/* @internal */
469468
export function isBlockScope(node: Node, parentNode: Node) {
470469
switch (node.kind) {
471470
case SyntaxKind.SourceFile:
@@ -493,7 +492,6 @@ namespace ts {
493492
return false;
494493
}
495494

496-
/* @internal */
497495
export function isDeclarationWithTypeParameters(node: Node): node is DeclarationWithTypeParameters;
498496
export function isDeclarationWithTypeParameters(node: DeclarationWithTypeParameters): node is DeclarationWithTypeParameters {
499497
switch (node.kind) {
@@ -523,7 +521,6 @@ namespace ts {
523521
}
524522
}
525523

526-
/* @internal */
527524
export function isAnyImportSyntax(node: Node): node is AnyImportSyntax {
528525
switch (node.kind) {
529526
case SyntaxKind.ImportDeclaration:
@@ -1805,7 +1802,6 @@ namespace ts {
18051802
}
18061803
}
18071804

1808-
/* @internal */
18091805
// See GH#16030
18101806
export function isAnyDeclarationName(name: Node): boolean {
18111807
switch (name.kind) {
@@ -3115,7 +3111,6 @@ namespace ts {
31153111
return flags;
31163112
}
31173113

3118-
/* @internal */
31193114
export function getModifierFlagsNoCache(node: Node): ModifierFlags {
31203115

31213116
let flags = ModifierFlags.None;
@@ -3677,6 +3672,27 @@ namespace ts {
36773672
}
36783673
}
36793674

3675+
// Return true if the given type is the constructor type for an abstract class
3676+
export function isAbstractConstructorType(type: Type): boolean {
3677+
return !!(getObjectFlags(type) & ObjectFlags.Anonymous) && !!type.symbol && isAbstractConstructorSymbol(type.symbol);
3678+
}
3679+
3680+
export function isAbstractConstructorSymbol(symbol: Symbol): boolean {
3681+
if (symbol.flags & SymbolFlags.Class) {
3682+
const declaration = getClassLikeDeclarationOfSymbol(symbol);
3683+
return !!declaration && hasModifier(declaration, ModifierFlags.Abstract);
3684+
}
3685+
return false;
3686+
}
3687+
3688+
export function getClassLikeDeclarationOfSymbol(symbol: Symbol): Declaration | undefined {
3689+
return find(symbol.declarations, isClassLike);
3690+
}
3691+
3692+
export function getObjectFlags(type: Type): ObjectFlags {
3693+
return type.flags & TypeFlags.Object ? (<ObjectType>type).objectFlags : 0;
3694+
}
3695+
36803696
export function typeHasCallOrConstructSignatures(type: Type, checker: TypeChecker) {
36813697
return checker.getSignaturesOfType(type, SignatureKind.Call).length !== 0 || checker.getSignaturesOfType(type, SignatureKind.Construct).length !== 0;
36823698
}

src/harness/fourslash.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ namespace FourSlash {
888888
* @param expectedKind the kind of symbol (see ScriptElementKind)
889889
* @param spanIndex the index of the range that the completion item's replacement text span should match
890890
*/
891-
public verifyCompletionListDoesNotContain(entryId: ts.Completions.CompletionEntryIdentifier, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number, options?: ts.GetCompletionsAtPositionOptions) {
891+
public verifyCompletionListDoesNotContain(entryId: ts.Completions.CompletionEntryIdentifier, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number, options?: FourSlashInterface.CompletionsAtOptions) {
892892
let replacementSpan: ts.TextSpan;
893893
if (spanIndex !== undefined) {
894894
replacementSpan = this.getTextSpanForRangeAtIndex(spanIndex);
@@ -1207,7 +1207,7 @@ Actual: ${stringify(fullActual)}`);
12071207
this.raiseError(`verifyReferencesAtPositionListContains failed - could not find the item: ${stringify(missingItem)} in the returned list: (${stringify(references)})`);
12081208
}
12091209

1210-
private getCompletionListAtCaret(options?: ts.GetCompletionsAtPositionOptions): ts.CompletionInfo {
1210+
private getCompletionListAtCaret(options?: FourSlashInterface.CompletionsAtOptions): ts.CompletionInfo {
12111211
return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options);
12121212
}
12131213

@@ -1721,7 +1721,7 @@ Actual: ${stringify(fullActual)}`);
17211721
const longestNameLength = max(entries, m => m.name.length);
17221722
const longestKindLength = max(entries, m => m.kind.length);
17231723
entries.sort((m, n) => m.sortText > n.sortText ? 1 : m.sortText < n.sortText ? -1 : m.name > n.name ? 1 : m.name < n.name ? -1 : 0);
1724-
const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers} ${m.source === undefined ? "" : m.source}`).join("\n");
1724+
const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers} ${m.isRecommended ? "recommended " : ""}${m.source === undefined ? "" : m.source}`).join("\n");
17251725
Harness.IO.log(membersString);
17261726
}
17271727

@@ -3114,6 +3114,7 @@ Actual: ${stringify(fullActual)}`);
31143114
}
31153115

31163116
assert.equal(item.hasAction, hasAction);
3117+
assert.equal(item.isRecommended, options && options.isRecommended, "isRecommended");
31173118
}
31183119

31193120
private findFile(indexOrName: string | number) {
@@ -4552,12 +4553,13 @@ namespace FourSlashInterface {
45524553
newContent: string;
45534554
}
45544555

4555-
export interface CompletionsAtOptions {
4556+
export interface CompletionsAtOptions extends ts.GetCompletionsAtPositionOptions {
45564557
isNewIdentifierLocation?: boolean;
45574558
}
45584559

45594560
export interface VerifyCompletionListContainsOptions extends ts.GetCompletionsAtPositionOptions {
45604561
sourceDisplay: string;
4562+
isRecommended?: true;
45614563
}
45624564

45634565
export interface NewContentOptions {

src/server/client.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,15 @@ namespace ts.server {
180180
isGlobalCompletion: false,
181181
isMemberCompletion: false,
182182
isNewIdentifierLocation: false,
183-
entries: response.body.map(entry => {
184-
183+
entries: response.body.map<CompletionEntry>(entry => {
185184
if (entry.replacementSpan !== undefined) {
186-
const { name, kind, kindModifiers, sortText, replacementSpan } = entry;
187-
return { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName) };
185+
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry;
186+
// TODO: GH#241
187+
const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended };
188+
return res;
188189
}
189190

190-
return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string };
191+
return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217
191192
})
192193
};
193194
}

src/server/protocol.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,8 +1701,9 @@ namespace ts.server.protocol {
17011701
*/
17021702
sortText: string;
17031703
/**
1704-
* An optional span that indicates the text to be replaced by this completion item. If present,
1705-
* this span should be used instead of the default one.
1704+
* An optional span that indicates the text to be replaced by this completion item.
1705+
* If present, this span should be used instead of the default one.
1706+
* It will be set if the required span differs from the one generated by the default replacement behavior.
17061707
*/
17071708
replacementSpan?: TextSpan;
17081709
/**
@@ -1714,6 +1715,12 @@ namespace ts.server.protocol {
17141715
* Identifier (not necessarily human-readable) identifying where this completion came from.
17151716
*/
17161717
source?: string;
1718+
/**
1719+
* If true, this completion should be highlighted as recommended. There will only be one of these.
1720+
* This will be set when we know the user should write an expression with a certain type and that type is an enum or constructable class.
1721+
* Then either that enum/class or a namespace containing it will be the recommended symbol.
1722+
*/
1723+
isRecommended?: true;
17171724
}
17181725

17191726
/**

src/server/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,10 +1207,10 @@ namespace ts.server {
12071207
if (simplifiedResult) {
12081208
return mapDefined<CompletionEntry, protocol.CompletionEntry>(completions && completions.entries, entry => {
12091209
if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) {
1210-
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source } = entry;
1210+
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry;
12111211
const convertedSpan = replacementSpan ? this.toLocationTextSpan(replacementSpan, scriptInfo) : undefined;
12121212
// Use `hasAction || undefined` to avoid serializing `false`.
1213-
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source };
1213+
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended };
12141214
}
12151215
}).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
12161216
}

0 commit comments

Comments
 (0)