From bd0405643a323b047296826a3302a061e03c007c Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 14 Nov 2017 11:38:15 -0800 Subject: [PATCH] Support a 'recommended' completion entry --- src/compiler/checker.ts | 58 ++++++++++--------- src/compiler/types.ts | 11 ++++ src/compiler/utilities.ts | 26 +++++++-- src/harness/fourslash.ts | 12 ++-- src/server/client.ts | 11 ++-- src/server/protocol.ts | 11 +++- src/server/session.ts | 4 +- src/services/completions.ts | 49 ++++++++++++++-- src/services/types.ts | 9 +-- .../reference/api/tsserverlibrary.d.ts | 17 +++--- tests/baselines/reference/api/typescript.d.ts | 6 +- .../completionsRecommended_equals.ts | 8 +++ .../completionsRecommended_import.ts | 27 +++++++++ .../fourslash/completionsRecommended_local.ts | 18 ++++++ .../completionsRecommended_namespace.ts | 33 +++++++++++ .../completionsRecommended_switch.ts | 10 ++++ tests/cases/fourslash/fourslash.ts | 2 +- 17 files changed, 242 insertions(+), 70 deletions(-) create mode 100644 tests/cases/fourslash/completionsRecommended_equals.ts create mode 100644 tests/cases/fourslash/completionsRecommended_import.ts create mode 100644 tests/cases/fourslash/completionsRecommended_local.ts create mode 100644 tests/cases/fourslash/completionsRecommended_namespace.ts create mode 100644 tests/cases/fourslash/completionsRecommended_switch.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6edce1036cb68..0a3dfd9cb743f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -254,6 +254,7 @@ namespace ts { return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); }, getJsxNamespace: () => unescapeLeadingUnderscores(getJsxNamespace()), + getAccessibleSymbolChain, }; const tupleTypes: GenericType[] = []; @@ -738,10 +739,6 @@ namespace ts { return nodeLinks[nodeId] || (nodeLinks[nodeId] = { flags: 0 }); } - function getObjectFlags(type: Type): ObjectFlags { - return type.flags & TypeFlags.Object ? (type).objectFlags : 0; - } - function isGlobalSourceFile(node: Node) { return node.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule(node); } @@ -10146,20 +10143,6 @@ namespace ts { !hasBaseType(checkClass, getDeclaringClass(p)) : false) ? undefined : checkClass; } - // Return true if the given type is the constructor type for an abstract class - function isAbstractConstructorType(type: Type) { - if (getObjectFlags(type) & ObjectFlags.Anonymous) { - const symbol = type.symbol; - if (symbol && symbol.flags & SymbolFlags.Class) { - const declaration = getClassLikeDeclarationOfSymbol(symbol); - if (declaration && hasModifier(declaration, ModifierFlags.Abstract)) { - return true; - } - } - } - return false; - } - // Return true if the given type is deeply nested. We consider this to be the case when structural type comparisons // for 5 or more occurrences or instantiations of the type have been recorded on the given stack. It is possible, // 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 { // the contextual type of an initializer expression is the type annotation of the containing declaration, if present. function getContextualTypeForInitializerExpression(node: Expression): Type { const declaration = node.parent; - if (node === declaration.initializer) { + if (node === declaration.initializer || node.kind === SyntaxKind.EqualsToken) { const typeNode = getEffectiveTypeAnnotationNode(declaration); if (typeNode) { return getTypeFromTypeNode(typeNode); @@ -13529,7 +13512,8 @@ namespace ts { function getContextualTypeForBinaryOperand(node: Expression): Type { const binaryExpression = node.parent; - const operator = binaryExpression.operatorToken.kind; + const { operatorToken } = binaryExpression; + const operator = operatorToken.kind; if (isAssignmentOperator(operator)) { if (node === binaryExpression.right) { // Don't do this for special property assignments to avoid circularity @@ -13567,10 +13551,26 @@ namespace ts { return getContextualType(binaryExpression); } } + else if (node === operatorToken && isEquationOperator(operator)) { + // For completions after `x === ` + return getTypeOfExpression(binaryExpression.left); + } return undefined; } + function isEquationOperator(operator: SyntaxKind) { + switch (operator) { + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + return true; + default: + return false; + } + } + function getTypeOfPropertyOfContextualType(type: Type, name: __String) { return mapType(type, t => { const prop = t.flags & TypeFlags.StructuredType ? getPropertyOfType(t, name) : undefined; @@ -13761,9 +13761,13 @@ namespace ts { return getContextualTypeForReturnExpression(node); case SyntaxKind.YieldExpression: return getContextualTypeForYieldOperand(parent); - case SyntaxKind.CallExpression: case SyntaxKind.NewExpression: - return getContextualTypeForArgument(parent, node); + if (node.kind === SyntaxKind.NewKeyword) { // for completions after `new ` + return getContextualType(parent as NewExpression); + } + // falls through + case SyntaxKind.CallExpression: + return getContextualTypeForArgument(parent, node); case SyntaxKind.TypeAssertionExpression: case SyntaxKind.AsExpression: return getTypeFromTypeNode((parent).type); @@ -13797,6 +13801,12 @@ namespace ts { case SyntaxKind.JsxOpeningElement: case SyntaxKind.JsxSelfClosingElement: return getAttributesTypeFromJsxOpeningLikeElement(parent); + case SyntaxKind.CaseClause: { + if (node.kind === SyntaxKind.CaseKeyword) { // for completions after `case ` + const switchStatement = (parent as CaseClause).parent.parent; + return getTypeOfExpression(switchStatement.expression); + } + } } return undefined; } @@ -22117,10 +22127,6 @@ namespace ts { return getCheckFlags(s) & CheckFlags.Instantiated ? (s).target : s; } - function getClassLikeDeclarationOfSymbol(symbol: Symbol): Declaration { - return forEach(symbol.declarations, d => isClassLike(d) ? d : undefined); - } - function getClassOrInterfaceDeclarationsOfSymbol(symbol: Symbol) { return filter(symbol.declarations, (d: Declaration): d is ClassDeclaration | InterfaceDeclaration => d.kind === SyntaxKind.ClassDeclaration || d.kind === SyntaxKind.InterfaceDeclaration); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 3141e3f22d924..0cd9e5db0a2e5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2781,6 +2781,17 @@ namespace ts { /* @internal */ getAllPossiblePropertiesOfTypes(type: ReadonlyArray): Symbol[]; /* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol | undefined; /* @internal */ getJsxNamespace(): string; + + /** + * Note that this will return undefined in the following case: + * // a.ts + * export namespace N { export class C { } } + * // b.ts + * <> + * Where `C` is the symbol we're looking for. + * This should be called in a loop climbing parents of the symbol, so we'll get `N`. + */ + /* @internal */ getAccessibleSymbolChain(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean): Symbol[] | undefined; } export enum NodeBuilderFlags { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e488cdcc601e1..cee907e12e050 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -464,7 +464,6 @@ namespace ts { return isExternalModule(node) || compilerOptions.isolatedModules || ((getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS) && !!node.commonJsModuleIndicator); } - /* @internal */ export function isBlockScope(node: Node, parentNode: Node) { switch (node.kind) { case SyntaxKind.SourceFile: @@ -492,7 +491,6 @@ namespace ts { return false; } - /* @internal */ export function isDeclarationWithTypeParameters(node: Node): node is DeclarationWithTypeParameters; export function isDeclarationWithTypeParameters(node: DeclarationWithTypeParameters): node is DeclarationWithTypeParameters { switch (node.kind) { @@ -522,7 +520,6 @@ namespace ts { } } - /* @internal */ export function isAnyImportSyntax(node: Node): node is AnyImportSyntax { switch (node.kind) { case SyntaxKind.ImportDeclaration: @@ -1742,7 +1739,6 @@ namespace ts { } } - /* @internal */ // See GH#16030 export function isAnyDeclarationName(name: Node): boolean { switch (name.kind) { @@ -3039,7 +3035,6 @@ namespace ts { return flags; } - /* @internal */ export function getModifierFlagsNoCache(node: Node): ModifierFlags { let flags = ModifierFlags.None; @@ -3623,6 +3618,27 @@ namespace ts { directory = parentPath; } } + + // Return true if the given type is the constructor type for an abstract class + export function isAbstractConstructorType(type: Type): boolean { + return !!(getObjectFlags(type) & ObjectFlags.Anonymous) && !!type.symbol && isAbstractConstructorSymbol(type.symbol); + } + + export function isAbstractConstructorSymbol(symbol: Symbol): boolean { + if (symbol.flags & SymbolFlags.Class) { + const declaration = getClassLikeDeclarationOfSymbol(symbol); + return !!declaration && hasModifier(declaration, ModifierFlags.Abstract); + } + return false; + } + + export function getClassLikeDeclarationOfSymbol(symbol: Symbol): Declaration | undefined { + return find(symbol.declarations, isClassLike); + } + + export function getObjectFlags(type: Type): ObjectFlags { + return type.flags & TypeFlags.Object ? (type).objectFlags : 0; + } } namespace ts { diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 6f08b900ec3c8..a49a0a805d96c 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -888,7 +888,7 @@ namespace FourSlash { * @param expectedKind the kind of symbol (see ScriptElementKind) * @param spanIndex the index of the range that the completion item's replacement text span should match */ - public verifyCompletionListDoesNotContain(entryId: ts.Completions.CompletionEntryIdentifier, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number, options?: ts.GetCompletionsAtPositionOptions) { + public verifyCompletionListDoesNotContain(entryId: ts.Completions.CompletionEntryIdentifier, expectedText?: string, expectedDocumentation?: string, expectedKind?: string, spanIndex?: number, options?: FourSlashInterface.CompletionsAtOptions) { let replacementSpan: ts.TextSpan; if (spanIndex !== undefined) { replacementSpan = this.getTextSpanForRangeAtIndex(spanIndex); @@ -1207,7 +1207,7 @@ Actual: ${stringify(fullActual)}`); this.raiseError(`verifyReferencesAtPositionListContains failed - could not find the item: ${stringify(missingItem)} in the returned list: (${stringify(references)})`); } - private getCompletionListAtCaret(options?: ts.GetCompletionsAtPositionOptions): ts.CompletionInfo { + private getCompletionListAtCaret(options?: FourSlashInterface.CompletionsAtOptions): ts.CompletionInfo { return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options); } @@ -1721,7 +1721,7 @@ Actual: ${stringify(fullActual)}`); const longestNameLength = max(entries, m => m.name.length); const longestKindLength = max(entries, m => m.kind.length); 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); - const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers} ${m.source === undefined ? "" : m.source}`).join("\n"); + 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"); Harness.IO.log(membersString); } @@ -3102,7 +3102,8 @@ Actual: ${stringify(fullActual)}`); assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + entryId)); } - assert.equal(item.hasAction, hasAction); + assert.equal(item.hasAction, hasAction, "hasAction"); + assert.equal(item.isRecommended, options && options.isRecommended, "isRecommended"); return; } @@ -4549,12 +4550,13 @@ namespace FourSlashInterface { newContent: string; } - export interface CompletionsAtOptions { + export interface CompletionsAtOptions extends ts.GetCompletionsAtPositionOptions { isNewIdentifierLocation?: boolean; } export interface VerifyCompletionListContainsOptions extends ts.GetCompletionsAtPositionOptions { sourceDisplay: string; + isRecommended?: true; } export interface NewContentOptions { diff --git a/src/server/client.ts b/src/server/client.ts index 8286119e8289e..4439e972c1740 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -180,14 +180,15 @@ namespace ts.server { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, - entries: response.body.map(entry => { - + entries: response.body.map(entry => { if (entry.replacementSpan !== undefined) { - const { name, kind, kindModifiers, sortText, replacementSpan } = entry; - return { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName) }; + const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; + // TODO: GH#241 + const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; + return res; } - return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; + return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 }) }; } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 3761049017d03..7bab9cf5abd5d 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1701,8 +1701,9 @@ namespace ts.server.protocol { */ sortText: string; /** - * An optional span that indicates the text to be replaced by this completion item. If present, - * this span should be used instead of the default one. + * An optional span that indicates the text to be replaced by this completion item. + * If present, this span should be used instead of the default one. + * It will be set if the required span differs from the one generated by the default replacement behavior. */ replacementSpan?: TextSpan; /** @@ -1714,6 +1715,12 @@ namespace ts.server.protocol { * Identifier (not necessarily human-readable) identifying where this completion came from. */ source?: string; + /** + * If true, this completion should be highlighted as recommended. There will only be one of these. + * 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. + * Then either that enum/class or a namespace containing it will be the recommended symbol. + */ + isRecommended?: true; } /** diff --git a/src/server/session.ts b/src/server/session.ts index f9a145d16fdb5..52672cd9e9b5b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1204,10 +1204,10 @@ namespace ts.server { if (simplifiedResult) { return mapDefined(completions && completions.entries, entry => { if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) { - const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source } = entry; + const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; const convertedSpan = replacementSpan ? this.toLocationTextSpan(replacementSpan, scriptInfo) : undefined; // Use `hasAction || undefined` to avoid serializing `false`. - return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source }; + return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended }; } }).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name)); } diff --git a/src/services/completions.ts b/src/services/completions.ts index 423919f535360..92a4e2095b58f 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -44,7 +44,7 @@ namespace ts.Completions { return undefined; } - const { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, request, keywordFilters, symbolToOriginInfoMap } = completionData; + const { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, request, keywordFilters, symbolToOriginInfoMap, recommendedCompletion } = completionData; if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) { @@ -76,7 +76,7 @@ namespace ts.Completions { const entries: CompletionEntry[] = []; if (isSourceFileJavaScript(sourceFile)) { - const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, symbolToOriginInfoMap); + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, recommendedCompletion, symbolToOriginInfoMap); getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target, entries); } else { @@ -84,7 +84,7 @@ namespace ts.Completions { return undefined; } - getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, symbolToOriginInfoMap); + getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, recommendedCompletion, symbolToOriginInfoMap); } // TODO add filter for keyword based on type/value/namespace and also location @@ -137,6 +137,7 @@ namespace ts.Completions { target: ScriptTarget, allowStringLiteral: boolean, origin: SymbolOriginInfo | undefined, + recommendedCompletion: Symbol | undefined, ): CompletionEntry | undefined { // Try to get a valid display name for this symbol, if we could not find one, then ignore it. // We would like to only show things that can be added after a dot, so for instance numeric properties can @@ -160,10 +161,20 @@ namespace ts.Completions { kindModifiers: SymbolDisplay.getSymbolModifiers(symbol), sortText: "0", source: getSourceFromOrigin(origin), - hasAction: origin === undefined ? undefined : true, + hasAction: trueOrUndef(origin !== undefined), + isRecommended: trueOrUndef(isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker)), }; } + function isRecommendedCompletionMatch(localSymbol: Symbol, recommendedCompletion: Symbol, checker: TypeChecker): boolean { + return localSymbol === recommendedCompletion || + !!(localSymbol.flags & SymbolFlags.ExportValue) && checker.getExportSymbolOfSymbol(localSymbol) === recommendedCompletion; + } + + function trueOrUndef(b: boolean): true | undefined { + return b ? true : undefined; + } + function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined { return origin && stripQuotes(origin.moduleSymbol.name); } @@ -177,6 +188,7 @@ namespace ts.Completions { target: ScriptTarget, log: Log, allowStringLiteral: boolean, + recommendedCompletion?: Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, ): Map { const start = timestamp(); @@ -188,7 +200,7 @@ namespace ts.Completions { if (symbols) { for (const symbol of symbols) { const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; - const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target, allowStringLiteral, origin); + const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target, allowStringLiteral, origin, recommendedCompletion); if (!entry) { continue; } @@ -524,9 +536,32 @@ namespace ts.Completions { request?: Request; keywordFilters: KeywordCompletionFilters; symbolToOriginInfoMap: SymbolOriginInfoMap; + recommendedCompletion: Symbol | undefined; } type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag }; + function getRecommendedCompletion(currentToken: Node, checker: TypeChecker/*, symbolToOriginInfoMap: SymbolOriginInfoMap*/): Symbol | undefined { + const ty = checker.getContextualType(currentToken as Expression); + const symbol = ty && ty.symbol; + // Don't include make a recommended completion for an abstract class + return symbol && (symbol.flags & SymbolFlags.Enum || symbol.flags & SymbolFlags.Class && !isAbstractConstructorSymbol(symbol)) + ? getFirstSymbolInChain(symbol, currentToken, checker) + : undefined; + } + + function getFirstSymbolInChain(symbol: Symbol, enclosingDeclaration: Node, checker: TypeChecker): Symbol | undefined { + const chain = checker.getAccessibleSymbolChain(symbol, enclosingDeclaration, /*meaning*/ SymbolFlags.All, /*useOnlyExternalAliasing*/ false); + return chain + ? first(chain) + : isModuleSymbol(symbol.parent) + ? symbol + : symbol.parent && getFirstSymbolInChain(symbol.parent, enclosingDeclaration, checker); + } + + function isModuleSymbol(symbol: Symbol): boolean { + return symbol.declarations.some(d => d.kind === SyntaxKind.SourceFile); + } + function getCompletionData( typeChecker: TypeChecker, log: (message: string) => void, @@ -618,6 +653,7 @@ namespace ts.Completions { request, keywordFilters: KeywordCompletionFilters.None, symbolToOriginInfoMap: undefined, + recommendedCompletion: undefined, }; } @@ -757,7 +793,8 @@ namespace ts.Completions { log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters, symbolToOriginInfoMap }; + const recommendedCompletion = getRecommendedCompletion(previousToken, typeChecker); + return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters, symbolToOriginInfoMap, recommendedCompletion }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; diff --git a/src/services/types.ts b/src/services/types.ts index bcebc43789261..75238ac63730b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -705,19 +705,16 @@ namespace ts { entries: CompletionEntry[]; } + // see comments in protocol.ts export interface CompletionEntry { name: string; kind: ScriptElementKind; - kindModifiers: string; // see ScriptElementKindModifier, comma separated + kindModifiers: string; sortText: string; - /** - * An optional span that indicates the text to be replaced by this completion item. It will be - * set if the required span differs from the one generated by the default replacement behavior and should - * be used in that case - */ replacementSpan?: TextSpan; hasAction?: true; source?: string; + isRecommended?: true; } export interface CompletionEntryDetails { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 096ad71c89424..bed9969abaad5 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4318,14 +4318,10 @@ declare namespace ts { kind: ScriptElementKind; kindModifiers: string; sortText: string; - /** - * An optional span that indicates the text to be replaced by this completion item. It will be - * set if the required span differs from the one generated by the default replacement behavior and should - * be used in that case - */ replacementSpan?: TextSpan; hasAction?: true; source?: string; + isRecommended?: true; } interface CompletionEntryDetails { name: string; @@ -6127,8 +6123,9 @@ declare namespace ts.server.protocol { */ sortText: string; /** - * An optional span that indicates the text to be replaced by this completion item. If present, - * this span should be used instead of the default one. + * An optional span that indicates the text to be replaced by this completion item. + * If present, this span should be used instead of the default one. + * It will be set if the required span differs from the one generated by the default replacement behavior. */ replacementSpan?: TextSpan; /** @@ -6140,6 +6137,12 @@ declare namespace ts.server.protocol { * Identifier (not necessarily human-readable) identifying where this completion came from. */ source?: string; + /** + * If true, this completion should be highlighted as recommended. There will only be one of these. + * 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. + * Then either that enum/class or a namespace containing it will be the recommended symbol. + */ + isRecommended?: true; } /** * Additional completion entry details, available on demand diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index dcc077d2cd700..00b22e56dc9fd 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4318,14 +4318,10 @@ declare namespace ts { kind: ScriptElementKind; kindModifiers: string; sortText: string; - /** - * An optional span that indicates the text to be replaced by this completion item. It will be - * set if the required span differs from the one generated by the default replacement behavior and should - * be used in that case - */ replacementSpan?: TextSpan; hasAction?: true; source?: string; + isRecommended?: true; } interface CompletionEntryDetails { name: string; diff --git a/tests/cases/fourslash/completionsRecommended_equals.ts b/tests/cases/fourslash/completionsRecommended_equals.ts new file mode 100644 index 0000000000000..21b17677ccc7e --- /dev/null +++ b/tests/cases/fourslash/completionsRecommended_equals.ts @@ -0,0 +1,8 @@ +/// + +////enum E {} +////declare const e: E; +////e === /**/ + +goTo.marker(); +verify.completionListContains("E", "enum E", "", "enum", undefined, undefined, { isRecommended: true }); diff --git a/tests/cases/fourslash/completionsRecommended_import.ts b/tests/cases/fourslash/completionsRecommended_import.ts new file mode 100644 index 0000000000000..dc8066d246e6a --- /dev/null +++ b/tests/cases/fourslash/completionsRecommended_import.ts @@ -0,0 +1,27 @@ +/// + +// @noLib: true + +// @Filename: /a.ts +////export class C {} +////export function f(c: C) {} + +// @Filename: /b.ts +////import { f } from "./a"; +// Here we will recommend a new import of 'C' +////f(new /*b*/); + +// @Filename: /c.ts +////import * as a from "./a"; +// Here we will recommend 'a' because it contains 'C'. +////a.f(new /*c*/); + +goTo.marker("b"); +verify.completionListContains({ name: "C", source: "/a" }, "class C", "", "class", undefined, /*hasAction*/ true, { + includeExternalModuleExports: true, + isRecommended: true, + sourceDisplay: "./a", +}); + +goTo.marker("c"); +verify.completionListContains("a", "import a", "", "alias", undefined, undefined, { isRecommended: true }); diff --git a/tests/cases/fourslash/completionsRecommended_local.ts b/tests/cases/fourslash/completionsRecommended_local.ts new file mode 100644 index 0000000000000..48cb412d88170 --- /dev/null +++ b/tests/cases/fourslash/completionsRecommended_local.ts @@ -0,0 +1,18 @@ +/// + +////enum E {} +////class C {} +////abstract class A {} +////const e: E = /*e*/ +////const c: C = new /*c*/ +////const a: A = new /*a*/ + +goTo.marker("e"); +verify.completionListContains("E", "enum E", "", "enum", undefined, undefined, { isRecommended: true }); + +goTo.marker("c"); +verify.completionListContains("C", "class C", "", "class", undefined, undefined, { isRecommended: true }); + +goTo.marker("i"); +// Not recommended, because it's an abstract class +verify.completionListContains("A", "class A", "", "class"); diff --git a/tests/cases/fourslash/completionsRecommended_namespace.ts b/tests/cases/fourslash/completionsRecommended_namespace.ts new file mode 100644 index 0000000000000..3ea8d597210d7 --- /dev/null +++ b/tests/cases/fourslash/completionsRecommended_namespace.ts @@ -0,0 +1,33 @@ +/// + +// @noLib: true + +// @Filename: /a.ts +////export namespace N { +//// export class C {} +////} +////export function f(c: N.C) {} +////f(new /*a*/); + +// @Filename: /b.ts +////import { f } from "./a"; +// Here we will recommend a new import of 'N' +////f(new /*b*/); + +// @Filename: /c.ts +////import * as a from "./a"; +// Here we will recommend 'a' because it contains 'N' which contains 'C'. +////a.f(new /*c*/); + +goTo.marker("a"); +verify.completionListContains("N", "namespace N", "", "module", undefined, undefined, { isRecommended: true }); + +goTo.marker("b"); +verify.completionListContains({ name: "N", source: "/a" }, "namespace N", "", "module", undefined, /*hasAction*/ true, { + includeExternalModuleExports: true, + isRecommended: true, + sourceDisplay: "./a", +}); + +goTo.marker("c"); +verify.completionListContains("a", "import a", "", "alias", undefined, undefined, { isRecommended: true }); diff --git a/tests/cases/fourslash/completionsRecommended_switch.ts b/tests/cases/fourslash/completionsRecommended_switch.ts new file mode 100644 index 0000000000000..b73d0be632ba6 --- /dev/null +++ b/tests/cases/fourslash/completionsRecommended_switch.ts @@ -0,0 +1,10 @@ +/// + +////enum E {} +////declare const e: E; +////switch (e) { +//// case /**/ +////} + +goTo.marker(); +verify.completionListContains("E", "enum E", "", "enum", undefined, undefined, { isRecommended: true }); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index f5984712648e3..01babe7ba3af4 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -148,7 +148,7 @@ declare namespace FourSlashInterface { kind?: string, spanIndex?: number, hasAction?: boolean, - options?: { includeExternalModuleExports: boolean, sourceDisplay: string }, + options?: { includeExternalModuleExports?: boolean, sourceDisplay?: string, isRecommended?: true }, ): void; completionListItemsCountIsGreaterThan(count: number): void; completionListIsEmpty(): void;