Skip to content

Commit bd04056

Browse files
author
Andy Hanson
committed
Support a 'recommended' completion entry
1 parent 592ee00 commit bd04056

17 files changed

+242
-70
lines changed

Diff for: src/compiler/checker.ts

+32-26
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ namespace ts {
254254
return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false);
255255
},
256256
getJsxNamespace: () => unescapeLeadingUnderscores(getJsxNamespace()),
257+
getAccessibleSymbolChain,
257258
};
258259

259260
const tupleTypes: GenericType[] = [];
@@ -738,10 +739,6 @@ namespace ts {
738739
return nodeLinks[nodeId] || (nodeLinks[nodeId] = { flags: 0 });
739740
}
740741

741-
function getObjectFlags(type: Type): ObjectFlags {
742-
return type.flags & TypeFlags.Object ? (<ObjectType>type).objectFlags : 0;
743-
}
744-
745742
function isGlobalSourceFile(node: Node) {
746743
return node.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule(<SourceFile>node);
747744
}
@@ -10146,20 +10143,6 @@ namespace ts {
1014610143
!hasBaseType(checkClass, getDeclaringClass(p)) : false) ? undefined : checkClass;
1014710144
}
1014810145

10149-
// Return true if the given type is the constructor type for an abstract class
10150-
function isAbstractConstructorType(type: Type) {
10151-
if (getObjectFlags(type) & ObjectFlags.Anonymous) {
10152-
const symbol = type.symbol;
10153-
if (symbol && symbol.flags & SymbolFlags.Class) {
10154-
const declaration = getClassLikeDeclarationOfSymbol(symbol);
10155-
if (declaration && hasModifier(declaration, ModifierFlags.Abstract)) {
10156-
return true;
10157-
}
10158-
}
10159-
}
10160-
return false;
10161-
}
10162-
1016310146
// Return true if the given type is deeply nested. We consider this to be the case when structural type comparisons
1016410147
// for 5 or more occurrences or instantiations of the type have been recorded on the given stack. It is possible,
1016510148
// though highly unlikely, for this test to be true in a situation where a chain of instantiations is not infinitely
@@ -13409,7 +13392,7 @@ namespace ts {
1340913392
// the contextual type of an initializer expression is the type annotation of the containing declaration, if present.
1341013393
function getContextualTypeForInitializerExpression(node: Expression): Type {
1341113394
const declaration = <VariableLikeDeclaration>node.parent;
13412-
if (node === declaration.initializer) {
13395+
if (node === declaration.initializer || node.kind === SyntaxKind.EqualsToken) {
1341313396
const typeNode = getEffectiveTypeAnnotationNode(declaration);
1341413397
if (typeNode) {
1341513398
return getTypeFromTypeNode(typeNode);
@@ -13529,7 +13512,8 @@ namespace ts {
1352913512

1353013513
function getContextualTypeForBinaryOperand(node: Expression): Type {
1353113514
const binaryExpression = <BinaryExpression>node.parent;
13532-
const operator = binaryExpression.operatorToken.kind;
13515+
const { operatorToken } = binaryExpression;
13516+
const operator = operatorToken.kind;
1353313517
if (isAssignmentOperator(operator)) {
1353413518
if (node === binaryExpression.right) {
1353513519
// Don't do this for special property assignments to avoid circularity
@@ -13567,10 +13551,26 @@ namespace ts {
1356713551
return getContextualType(binaryExpression);
1356813552
}
1356913553
}
13554+
else if (node === operatorToken && isEquationOperator(operator)) {
13555+
// For completions after `x === `
13556+
return getTypeOfExpression(binaryExpression.left);
13557+
}
1357013558

1357113559
return undefined;
1357213560
}
1357313561

13562+
function isEquationOperator(operator: SyntaxKind) {
13563+
switch (operator) {
13564+
case SyntaxKind.EqualsEqualsEqualsToken:
13565+
case SyntaxKind.EqualsEqualsToken:
13566+
case SyntaxKind.ExclamationEqualsEqualsToken:
13567+
case SyntaxKind.ExclamationEqualsToken:
13568+
return true;
13569+
default:
13570+
return false;
13571+
}
13572+
}
13573+
1357413574
function getTypeOfPropertyOfContextualType(type: Type, name: __String) {
1357513575
return mapType(type, t => {
1357613576
const prop = t.flags & TypeFlags.StructuredType ? getPropertyOfType(t, name) : undefined;
@@ -13761,9 +13761,13 @@ namespace ts {
1376113761
return getContextualTypeForReturnExpression(node);
1376213762
case SyntaxKind.YieldExpression:
1376313763
return getContextualTypeForYieldOperand(<YieldExpression>parent);
13764-
case SyntaxKind.CallExpression:
1376513764
case SyntaxKind.NewExpression:
13766-
return getContextualTypeForArgument(<CallExpression>parent, node);
13765+
if (node.kind === SyntaxKind.NewKeyword) { // for completions after `new `
13766+
return getContextualType(parent as NewExpression);
13767+
}
13768+
// falls through
13769+
case SyntaxKind.CallExpression:
13770+
return getContextualTypeForArgument(<CallExpression | NewExpression>parent, node);
1376713771
case SyntaxKind.TypeAssertionExpression:
1376813772
case SyntaxKind.AsExpression:
1376913773
return getTypeFromTypeNode((<AssertionExpression>parent).type);
@@ -13797,6 +13801,12 @@ namespace ts {
1379713801
case SyntaxKind.JsxOpeningElement:
1379813802
case SyntaxKind.JsxSelfClosingElement:
1379913803
return getAttributesTypeFromJsxOpeningLikeElement(<JsxOpeningLikeElement>parent);
13804+
case SyntaxKind.CaseClause: {
13805+
if (node.kind === SyntaxKind.CaseKeyword) { // for completions after `case `
13806+
const switchStatement = (parent as CaseClause).parent.parent;
13807+
return getTypeOfExpression(switchStatement.expression);
13808+
}
13809+
}
1380013810
}
1380113811
return undefined;
1380213812
}
@@ -22117,10 +22127,6 @@ namespace ts {
2211722127
return getCheckFlags(s) & CheckFlags.Instantiated ? (<TransientSymbol>s).target : s;
2211822128
}
2211922129

22120-
function getClassLikeDeclarationOfSymbol(symbol: Symbol): Declaration {
22121-
return forEach(symbol.declarations, d => isClassLike(d) ? d : undefined);
22122-
}
22123-
2212422130
function getClassOrInterfaceDeclarationsOfSymbol(symbol: Symbol) {
2212522131
return filter(symbol.declarations, (d: Declaration): d is ClassDeclaration | InterfaceDeclaration =>
2212622132
d.kind === SyntaxKind.ClassDeclaration || d.kind === SyntaxKind.InterfaceDeclaration);

Diff for: src/compiler/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -2781,6 +2781,17 @@ namespace ts {
27812781
/* @internal */ getAllPossiblePropertiesOfTypes(type: ReadonlyArray<Type>): Symbol[];
27822782
/* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol | undefined;
27832783
/* @internal */ getJsxNamespace(): string;
2784+
2785+
/**
2786+
* Note that this will return undefined in the following case:
2787+
* // a.ts
2788+
* export namespace N { export class C { } }
2789+
* // b.ts
2790+
* <<enclosingDeclaration>>
2791+
* Where `C` is the symbol we're looking for.
2792+
* This should be called in a loop climbing parents of the symbol, so we'll get `N`.
2793+
*/
2794+
/* @internal */ getAccessibleSymbolChain(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean): Symbol[] | undefined;
27842795
}
27852796

27862797
export enum NodeBuilderFlags {

Diff for: src/compiler/utilities.ts

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

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

495-
/* @internal */
496494
export function isDeclarationWithTypeParameters(node: Node): node is DeclarationWithTypeParameters;
497495
export function isDeclarationWithTypeParameters(node: DeclarationWithTypeParameters): node is DeclarationWithTypeParameters {
498496
switch (node.kind) {
@@ -522,7 +520,6 @@ namespace ts {
522520
}
523521
}
524522

525-
/* @internal */
526523
export function isAnyImportSyntax(node: Node): node is AnyImportSyntax {
527524
switch (node.kind) {
528525
case SyntaxKind.ImportDeclaration:
@@ -1742,7 +1739,6 @@ namespace ts {
17421739
}
17431740
}
17441741

1745-
/* @internal */
17461742
// See GH#16030
17471743
export function isAnyDeclarationName(name: Node): boolean {
17481744
switch (name.kind) {
@@ -3039,7 +3035,6 @@ namespace ts {
30393035
return flags;
30403036
}
30413037

3042-
/* @internal */
30433038
export function getModifierFlagsNoCache(node: Node): ModifierFlags {
30443039

30453040
let flags = ModifierFlags.None;
@@ -3623,6 +3618,27 @@ namespace ts {
36233618
directory = parentPath;
36243619
}
36253620
}
3621+
3622+
// Return true if the given type is the constructor type for an abstract class
3623+
export function isAbstractConstructorType(type: Type): boolean {
3624+
return !!(getObjectFlags(type) & ObjectFlags.Anonymous) && !!type.symbol && isAbstractConstructorSymbol(type.symbol);
3625+
}
3626+
3627+
export function isAbstractConstructorSymbol(symbol: Symbol): boolean {
3628+
if (symbol.flags & SymbolFlags.Class) {
3629+
const declaration = getClassLikeDeclarationOfSymbol(symbol);
3630+
return !!declaration && hasModifier(declaration, ModifierFlags.Abstract);
3631+
}
3632+
return false;
3633+
}
3634+
3635+
export function getClassLikeDeclarationOfSymbol(symbol: Symbol): Declaration | undefined {
3636+
return find(symbol.declarations, isClassLike);
3637+
}
3638+
3639+
export function getObjectFlags(type: Type): ObjectFlags {
3640+
return type.flags & TypeFlags.Object ? (<ObjectType>type).objectFlags : 0;
3641+
}
36263642
}
36273643

36283644
namespace ts {

Diff for: src/harness/fourslash.ts

+7-5
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

@@ -3102,7 +3102,8 @@ Actual: ${stringify(fullActual)}`);
31023102
assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + entryId));
31033103
}
31043104

3105-
assert.equal(item.hasAction, hasAction);
3105+
assert.equal(item.hasAction, hasAction, "hasAction");
3106+
assert.equal(item.isRecommended, options && options.isRecommended, "isRecommended");
31063107

31073108
return;
31083109
}
@@ -4549,12 +4550,13 @@ namespace FourSlashInterface {
45494550
newContent: string;
45504551
}
45514552

4552-
export interface CompletionsAtOptions {
4553+
export interface CompletionsAtOptions extends ts.GetCompletionsAtPositionOptions {
45534554
isNewIdentifierLocation?: boolean;
45544555
}
45554556

45564557
export interface VerifyCompletionListContainsOptions extends ts.GetCompletionsAtPositionOptions {
45574558
sourceDisplay: string;
4559+
isRecommended?: true;
45584560
}
45594561

45604562
export interface NewContentOptions {

Diff for: src/server/client.ts

+6-5
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
}

Diff for: src/server/protocol.ts

+9-2
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
/**

Diff for: src/server/session.ts

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

0 commit comments

Comments
 (0)