diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 29ebfde3f9..8d77440dc0 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -729,6 +729,7 @@ namespace ts { isDeclarationVisible, isPropertyAccessible, getTypeOnlyAliasDeclaration, + getMemberOverrideModifierStatus, }; function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): Signature | undefined { @@ -38469,7 +38470,7 @@ namespace ts { } } - checkMembersForMissingOverrideModifier(node, type, typeWithThis, staticType); + checkMembersForOverrideModifier(node, type, typeWithThis, staticType); const implementedTypeNodes = getEffectiveImplementsTypeNodes(node); if (implementedTypeNodes) { @@ -38506,8 +38507,7 @@ namespace ts { } } - function checkMembersForMissingOverrideModifier(node: ClassLikeDeclaration, type: InterfaceType, typeWithThis: Type, staticType: ObjectType) { - const nodeInAmbientContext = !!(node.flags & NodeFlags.Ambient); + function checkMembersForOverrideModifier(node: ClassLikeDeclaration, type: InterfaceType, typeWithThis: Type, staticType: ObjectType) { const baseTypeNode = getEffectiveBaseTypeNode(node); const baseTypes = baseTypeNode && getBaseTypes(type); const baseWithThis = baseTypes?.length ? getTypeWithThisArgument(first(baseTypes), type.thisType) : undefined; @@ -38521,53 +38521,130 @@ namespace ts { if (isConstructorDeclaration(member)) { forEach(member.parameters, param => { if (isParameterPropertyDeclaration(param, member)) { - checkClassMember(param, /*memberIsParameterProperty*/ true); + checkExistingMemberForOverrideModifier( + node, + staticType, + baseStaticType, + baseWithThis, + type, + typeWithThis, + param, + /* memberIsParameterProperty */ true + ); } }); } - checkClassMember(member); + checkExistingMemberForOverrideModifier( + node, + staticType, + baseStaticType, + baseWithThis, + type, + typeWithThis, + member, + /* memberIsParameterProperty */ false, + ); + } + } + + /** + * @param member Existing member node to be checked. + * Note: `member` cannot be a synthetic node. + */ + function checkExistingMemberForOverrideModifier( + node: ClassLikeDeclaration, + staticType: ObjectType, + baseStaticType: Type, + baseWithThis: Type | undefined, + type: InterfaceType, + typeWithThis: Type, + member: ClassElement | ParameterPropertyDeclaration, + memberIsParameterProperty: boolean, + reportErrors = true, + ): MemberOverrideStatus { + const declaredProp = member.name + && getSymbolAtLocation(member.name) + || getSymbolAtLocation(member); + if (!declaredProp) { + return MemberOverrideStatus.Ok; } - function checkClassMember(member: ClassElement | ParameterPropertyDeclaration, memberIsParameterProperty?: boolean) { - const hasOverride = hasOverrideModifier(member); - const hasStatic = isStatic(member); - const isJs = isInJSFile(member); - if (baseWithThis && (hasOverride || compilerOptions.noImplicitOverride)) { - const declaredProp = member.name && getSymbolAtLocation(member.name) || getSymbolAtLocation(member); - if (!declaredProp) { - return; - } + return checkMemberForOverrideModifier( + node, + staticType, + baseStaticType, + baseWithThis, + type, + typeWithThis, + hasOverrideModifier(member), + hasAbstractModifier(member), + isStatic(member), + memberIsParameterProperty, + symbolName(declaredProp), + reportErrors ? member : undefined, + ); + } - const thisType = hasStatic ? staticType : typeWithThis; - const baseType = hasStatic ? baseStaticType : baseWithThis; - const prop = getPropertyOfType(thisType, declaredProp.escapedName); - const baseProp = getPropertyOfType(baseType, declaredProp.escapedName); + /** + * Checks a class member declaration for either a missing or an invalid `override` modifier. + * Note: this function can be used for speculative checking, + * i.e. checking a member that does not yet exist in the program. + * An example of that would be to call this function in a completions scenario, + * when offering a method declaration as completion. + * @param errorNode The node where we should report an error, or undefined if we should not report errors. + */ + function checkMemberForOverrideModifier( + node: ClassLikeDeclaration, + staticType: ObjectType, + baseStaticType: Type, + baseWithThis: Type | undefined, + type: InterfaceType, + typeWithThis: Type, + memberHasOverrideModifier: boolean, + memberHasAbstractModifier: boolean, + memberIsStatic: boolean, + memberIsParameterProperty: boolean, + memberName: string, + errorNode?: Node, + ): MemberOverrideStatus { + const isJs = isInJSFile(node); + const nodeInAmbientContext = !!(node.flags & NodeFlags.Ambient); + if (baseWithThis && (memberHasOverrideModifier || compilerOptions.noImplicitOverride)) { + const memberEscapedName = escapeLeadingUnderscores(memberName); + const thisType = memberIsStatic ? staticType : typeWithThis; + const baseType = memberIsStatic ? baseStaticType : baseWithThis; + const prop = getPropertyOfType(thisType, memberEscapedName); + const baseProp = getPropertyOfType(baseType, memberEscapedName); - const baseClassName = typeToString(baseWithThis); - if (prop && !baseProp && hasOverride) { - const suggestion = getSuggestedSymbolForNonexistentClassMember(symbolName(declaredProp), baseType); + const baseClassName = typeToString(baseWithThis); + if (prop && !baseProp && memberHasOverrideModifier) { + if (errorNode) { + const suggestion = getSuggestedSymbolForNonexistentClassMember(memberName, baseType); // Again, using symbol name: note that's different from `symbol.escapedName` suggestion ? error( - member, + errorNode, isJs ? Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_it_is_not_declared_in_the_base_class_0_Did_you_mean_1 : Diagnostics.This_member_cannot_have_an_override_modifier_because_it_is_not_declared_in_the_base_class_0_Did_you_mean_1, baseClassName, symbolToString(suggestion)) : error( - member, + errorNode, isJs ? Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_it_is_not_declared_in_the_base_class_0 : Diagnostics.This_member_cannot_have_an_override_modifier_because_it_is_not_declared_in_the_base_class_0, baseClassName); } - else if (prop && baseProp?.declarations && compilerOptions.noImplicitOverride && !nodeInAmbientContext) { - const baseHasAbstract = some(baseProp.declarations, hasAbstractModifier); - if (hasOverride) { - return; - } + return MemberOverrideStatus.HasInvalidOverride; + } + else if (prop && baseProp?.declarations && compilerOptions.noImplicitOverride && !nodeInAmbientContext) { + const baseHasAbstract = some(baseProp.declarations, hasAbstractModifier); + if (memberHasOverrideModifier) { + return MemberOverrideStatus.Ok; + } - if (!baseHasAbstract) { + if (!baseHasAbstract) { + if (errorNode) { const diag = memberIsParameterProperty ? isJs ? Diagnostics.This_parameter_property_must_have_a_JSDoc_comment_with_an_override_tag_because_it_overrides_a_member_in_the_base_class_0 : @@ -38575,23 +38652,32 @@ namespace ts { isJs ? Diagnostics.This_member_must_have_a_JSDoc_comment_with_an_override_tag_because_it_overrides_a_member_in_the_base_class_0 : Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_a_member_in_the_base_class_0; - error(member, diag, baseClassName); + error(errorNode, diag, baseClassName); } - else if (hasAbstractModifier(member) && baseHasAbstract) { - error(member, Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_an_abstract_method_that_is_declared_in_the_base_class_0, baseClassName); + return MemberOverrideStatus.NeedsOverride; + } + else if (memberHasAbstractModifier && baseHasAbstract) { + if (errorNode) { + error(errorNode, Diagnostics.This_member_must_have_an_override_modifier_because_it_overrides_an_abstract_method_that_is_declared_in_the_base_class_0, baseClassName); } + return MemberOverrideStatus.NeedsOverride; } } - else if (hasOverride) { + } + else if (memberHasOverrideModifier) { + if (errorNode) { const className = typeToString(type); error( - member, + errorNode, isJs ? Diagnostics.This_member_cannot_have_a_JSDoc_comment_with_an_override_tag_because_its_containing_class_0_does_not_extend_another_class : Diagnostics.This_member_cannot_have_an_override_modifier_because_its_containing_class_0_does_not_extend_another_class, className); } + return MemberOverrideStatus.HasInvalidOverride; } + + return MemberOverrideStatus.Ok; } function issueMemberSpecificError(node: ClassLikeDeclaration, typeWithThis: Type, baseWithThis: Type, broadDiag: DiagnosticMessage) { @@ -38638,6 +38724,48 @@ namespace ts { } } + /** + * Checks a member declaration node to see if has a missing or invalid `override` modifier. + * @param node Class-like node where the member is declared. + * @param member Member declaration node. + * Note: `member` can be a synthetic node without a parent. + */ + function getMemberOverrideModifierStatus(node: ClassLikeDeclaration, member: ClassElement): MemberOverrideStatus { + if (!member.name) { + return MemberOverrideStatus.Ok; + } + + const symbol = getSymbolOfNode(node); + const type = getDeclaredTypeOfSymbol(symbol) as InterfaceType; + const typeWithThis = getTypeWithThisArgument(type); + const staticType = getTypeOfSymbol(symbol) as ObjectType; + + const baseTypeNode = getEffectiveBaseTypeNode(node); + const baseTypes = baseTypeNode && getBaseTypes(type); + const baseWithThis = baseTypes?.length ? getTypeWithThisArgument(first(baseTypes), type.thisType) : undefined; + const baseStaticType = getBaseConstructorTypeOfClass(type); + + const memberHasOverrideModifier = member.parent + ? hasOverrideModifier(member) + : hasSyntacticModifier(member, ModifierFlags.Override); + + const memberName = unescapeLeadingUnderscores(getTextOfPropertyName(member.name)); + + return checkMemberForOverrideModifier( + node, + staticType, + baseStaticType, + baseWithThis, + type, + typeWithThis, + memberHasOverrideModifier, + hasAbstractModifier(member), + isStatic(member), + /* memberIsParameterProperty */ false, + memberName, + ); + } + function getTargetSymbol(s: Symbol) { // if symbol is instantiated its flags are not copied from the 'target' // so we'll need to get back original 'target' symbol to work with correct set of flags diff --git a/src/compiler/debug.ts b/src/compiler/debug.ts index 2e6b87a30e..705ab94989 100644 --- a/src/compiler/debug.ts +++ b/src/compiler/debug.ts @@ -351,6 +351,10 @@ namespace ts { return formatEnum(kind, (ts as any).SyntaxKind, /*isFlags*/ false); } + export function formatSnippetKind(kind: SnippetKind | undefined): string { + return formatEnum(kind, (ts as any).SnippetKind, /*isFlags*/ false); + } + export function formatNodeFlags(flags: NodeFlags | undefined): string { return formatEnum(flags, (ts as any).NodeFlags, /*isFlags*/ true); } diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 105f09117c..d4eff1e0ac 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1283,7 +1283,13 @@ namespace ts { currentParenthesizerRule = undefined; } - function pipelineEmitWithHintWorker(hint: EmitHint, node: Node): void { + function pipelineEmitWithHintWorker(hint: EmitHint, node: Node, allowSnippets = true): void { + if (allowSnippets) { + const snippet = getSnippetElement(node); + if (snippet) { + return emitSnippetNode(hint, node, snippet); + } + } if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile)); if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier)); if (hint === EmitHint.JsxAttributeValue) return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true); @@ -1924,6 +1930,32 @@ namespace ts { } } + // + // Snippet Elements + // + + function emitSnippetNode(hint: EmitHint, node: Node, snippet: SnippetElement) { + switch (snippet.kind) { + case SnippetKind.Placeholder: + emitPlaceholder(hint, node, snippet); + break; + case SnippetKind.TabStop: + emitTabStop(snippet); + break; + } + } + + function emitPlaceholder(hint: EmitHint, node: Node, snippet: Placeholder) { + nonEscapingWrite(`\$\{${snippet.order}:`); // `${2:` + pipelineEmitWithHintWorker(hint, node, /*allowSnippets*/ false); // `...` + nonEscapingWrite(`\}`); // `}` + // `${2:...}` + } + + function emitTabStop(snippet: TabStop) { + nonEscapingWrite(`\$${snippet.order}`); + } + // // Identifiers // @@ -4457,6 +4489,16 @@ namespace ts { writer.writeProperty(s); } + function nonEscapingWrite(s: string) { + // This should be defined in a snippet-escaping text writer. + if (writer.nonEscapingWrite) { + writer.nonEscapingWrite(s); + } + else { + writer.write(s); + } + } + function writeLine(count = 1) { for (let i = 0; i < count; i++) { writer.writeLine(i > 0); diff --git a/src/compiler/factory/emitNode.ts b/src/compiler/factory/emitNode.ts index 8f032fd8b7..ebb7300efd 100644 --- a/src/compiler/factory/emitNode.ts +++ b/src/compiler/factory/emitNode.ts @@ -256,6 +256,24 @@ namespace ts { } } + /** + * Gets the SnippetElement of a node. + */ + /* @internal */ + export function getSnippetElement(node: Node): SnippetElement | undefined { + return node.emitNode?.snippetElement; + } + + /** + * Sets the SnippetElement of a node. + */ + /* @internal */ + export function setSnippetElement(node: T, snippet: SnippetElement): T { + const emitNode = getOrCreateEmitNode(node); + emitNode.snippetElement = snippet; + return node; + } + /* @internal */ export function ignoreSourceNewlines(node: T): T { getOrCreateEmitNode(node).flags |= EmitFlags.IgnoreSourceNewlines; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0af0d9d18c..b1690b6f78 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4414,6 +4414,14 @@ namespace ts { /* @internal */ isDeclarationVisible(node: Declaration | AnyImportSyntax): boolean; /* @internal */ isPropertyAccessible(node: Node, isSuper: boolean, isWrite: boolean, containingType: Type, property: Symbol): boolean; /* @internal */ getTypeOnlyAliasDeclaration(symbol: Symbol): TypeOnlyAliasDeclaration | undefined; + /* @internal */ getMemberOverrideModifierStatus(node: ClassLikeDeclaration, member: ClassElement): MemberOverrideStatus; + } + + /* @internal */ + export const enum MemberOverrideStatus { + Ok, + NeedsOverride, + HasInvalidOverride } /* @internal */ @@ -6816,6 +6824,31 @@ namespace ts { externalHelpers?: boolean; helpers?: EmitHelper[]; // Emit helpers for the node startsOnNewLine?: boolean; // If the node should begin on a new line + snippetElement?: SnippetElement; // Snippet element of the node + } + + /* @internal */ + export type SnippetElement = TabStop | Placeholder; + + /* @internal */ + export interface TabStop { + kind: SnippetKind.TabStop; + order: number; + } + + /* @internal */ + export interface Placeholder { + kind: SnippetKind.Placeholder; + order: number; + } + + // Reference: https://code.visualstudio.com/docs/editor/userdefinedsnippets#_snippet-syntax + /* @internal */ + export const enum SnippetKind { + TabStop, // `$1`, `$2` + Placeholder, // `${1:foo}` + Choice, // `${1|one,two,three|}` + Variable, // `$name`, `${name:default}` } export const enum EmitFlags { @@ -8278,6 +8311,7 @@ namespace ts { hasTrailingComment(): boolean; hasTrailingWhitespace(): boolean; getTextPosWithWriteLine?(): number; + nonEscapingWrite?(text: string): void; } export interface GetEffectiveTypeRootsHost { @@ -8623,6 +8657,7 @@ namespace ts { readonly includeCompletionsWithSnippetText?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; + readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 1743907474..8caba5b205 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3161,7 +3161,7 @@ namespace ts { return undefined; } - export function isKeyword(token: SyntaxKind): boolean { + export function isKeyword(token: SyntaxKind): token is KeywordSyntaxKind { return SyntaxKind.FirstKeyword <= token && token <= SyntaxKind.LastKeyword; } @@ -3346,7 +3346,7 @@ namespace ts { return node.escapedText === "push" || node.escapedText === "unshift"; } - export function isParameterDeclaration(node: VariableLikeDeclaration) { + export function isParameterDeclaration(node: VariableLikeDeclaration): boolean { const root = getRootDeclaration(node); return root.kind === SyntaxKind.Parameter; } @@ -7425,4 +7425,8 @@ namespace ts { export function isFunctionExpressionOrArrowFunction(node: Node): node is FunctionExpression | ArrowFunction { return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction; } + + export function escapeSnippetText(text: string): string { + return text.replace(/\$/gm, "\\$"); + } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 67d6fda2a3..35c3b783ee 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2289,7 +2289,7 @@ namespace ts.server.protocol { /** * Human-readable description of the `source`. */ - sourceDisplay?: SymbolDisplayPart[]; + sourceDisplay?: SymbolDisplayPart[]; /** * 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. @@ -3381,6 +3381,13 @@ namespace ts.server.protocol { * values, with insertion text to replace preceding `.` tokens with `?.`. */ readonly includeAutomaticOptionalChainCompletions?: boolean; + /** + * If enabled, completions for class members (e.g. methods and properties) will include + * a whole declaration for the member. + * E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of + * `class A { foo }`. + */ + readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts index 9e8313f357..18b61f9ecd 100644 --- a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts +++ b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts @@ -42,7 +42,7 @@ namespace ts.codefix { const abstractAndNonPrivateExtendsSymbols = checker.getPropertiesOfType(instantiatedExtendsType).filter(symbolPointsToNonPrivateAndAbstractMember); const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host); - createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, sourceFile, context, preferences, importAdder, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member)); + createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, sourceFile, context, preferences, importAdder, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member as ClassElement)); importAdder.writeFixes(changeTracker); } diff --git a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts index e1f17cb285..f090bdabf8 100644 --- a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts +++ b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts @@ -64,7 +64,7 @@ namespace ts.codefix { } const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host); - createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, sourceFile, context, preferences, importAdder, member => insertInterfaceMemberNode(sourceFile, classDeclaration, member)); + createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, sourceFile, context, preferences, importAdder, member => insertInterfaceMemberNode(sourceFile, classDeclaration, member as ClassElement)); importAdder.writeFixes(changeTracker); function createMissingIndexSignatureDeclaration(type: InterfaceType, kind: IndexKind): void { diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 5cdf6fb478..4564a571b1 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -7,11 +7,18 @@ namespace ts.codefix { * @param importAdder If provided, type annotations will use identifier type references instead of ImportTypeNodes, and the missing imports will be added to the importAdder. * @returns Empty string iff there are no member insertions. */ - export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: readonly Symbol[], sourceFile: SourceFile, context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: ClassElement) => void): void { + export function createMissingMemberNodes( + classDeclaration: ClassLikeDeclaration, + possiblyMissingSymbols: readonly Symbol[], + sourceFile: SourceFile, + context: TypeConstructionContext, + preferences: UserPreferences, + importAdder: ImportAdder | undefined, + addClassElement: (node: AddNode) => void): void { const classMembers = classDeclaration.symbol.members!; for (const symbol of possiblyMissingSymbols) { if (!classMembers.has(symbol.escapedName)) { - addNewNodeForMemberSymbol(symbol, classDeclaration, sourceFile, context, preferences, importAdder, addClassElement); + addNewNodeForMemberSymbol(symbol, classDeclaration, sourceFile, context, preferences, importAdder, addClassElement, /* body */ undefined); } } } @@ -28,10 +35,23 @@ namespace ts.codefix { host: LanguageServiceHost; } + type AddNode = PropertyDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | MethodDeclaration | FunctionExpression | ArrowFunction; + /** - * @returns Empty string iff there we can't figure out a representation for `symbol` in `enclosingDeclaration`. + * `addClassElement` will not be called if we can't figure out a representation for `symbol` in `enclosingDeclaration`. + * @param body If defined, this will be the body of the member node passed to `addClassElement`. Otherwise, the body will default to a stub. */ - function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, sourceFile: SourceFile, context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: Node) => void): void { + export function addNewNodeForMemberSymbol( + symbol: Symbol, + enclosingDeclaration: ClassLikeDeclaration, + sourceFile: SourceFile, + context: TypeConstructionContext, + preferences: UserPreferences, + importAdder: ImportAdder | undefined, + addClassElement: (node: AddNode) => void, + body: Block | undefined, + isAmbient = false, + ): void { const declarations = symbol.getDeclarations(); if (!(declarations && declarations.length)) { return undefined; @@ -44,7 +64,7 @@ namespace ts.codefix { const modifiers = visibilityModifier ? factory.createNodeArray([visibilityModifier]) : undefined; const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); const optional = !!(symbol.flags & SymbolFlags.Optional); - const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient); + const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient) || isAmbient; const quotePreference = getQuotePreference(sourceFile, preferences); switch (declaration.kind) { @@ -89,7 +109,7 @@ namespace ts.codefix { name, emptyArray, typeNode, - ambient ? undefined : createStubbedMethodBody(quotePreference))); + ambient ? undefined : body || createStubbedMethodBody(quotePreference))); } else { Debug.assertNode(accessor, isSetAccessorDeclaration, "The counterpart to a getter should be a setter"); @@ -100,7 +120,7 @@ namespace ts.codefix { modifiers, name, createDummyParameters(1, [parameterName], [typeNode], 1, /*inJs*/ false), - ambient ? undefined : createStubbedMethodBody(quotePreference))); + ambient ? undefined : body || createStubbedMethodBody(quotePreference))); } } break; @@ -122,7 +142,7 @@ namespace ts.codefix { if (declarations.length === 1) { Debug.assert(signatures.length === 1, "One declaration implies one signature"); const signature = signatures[0]; - outputMethod(quotePreference, signature, modifiers, name, ambient ? undefined : createStubbedMethodBody(quotePreference)); + outputMethod(quotePreference, signature, modifiers, name, ambient ? undefined : body || createStubbedMethodBody(quotePreference)); break; } @@ -134,11 +154,11 @@ namespace ts.codefix { if (!ambient) { if (declarations.length > signatures.length) { const signature = checker.getSignatureFromDeclaration(declarations[declarations.length - 1] as SignatureDeclaration)!; - outputMethod(quotePreference, signature, modifiers, name, createStubbedMethodBody(quotePreference)); + outputMethod(quotePreference, signature, modifiers, name, body || createStubbedMethodBody(quotePreference)); } else { Debug.assert(declarations.length === signatures.length, "Declarations and signatures should match count"); - addClassElement(createMethodImplementingSignatures(checker, context, enclosingDeclaration, signatures, name, optional, modifiers, quotePreference)); + addClassElement(createMethodImplementingSignatures(checker, context, enclosingDeclaration, signatures, name, optional, modifiers, quotePreference, body)); } } break; @@ -348,6 +368,7 @@ namespace ts.codefix { optional: boolean, modifiers: readonly Modifier[] | undefined, quotePreference: QuotePreference, + body: Block | undefined, ): MethodDeclaration { /** This is *a* signature with the maximal number of arguments, * such that if there is a "maximal" signature without rest arguments, @@ -389,7 +410,8 @@ namespace ts.codefix { /*typeParameters*/ undefined, parameters, getReturnTypeFromSignatures(signatures, checker, context, enclosingDeclaration), - quotePreference); + quotePreference, + body); } function getReturnTypeFromSignatures(signatures: readonly Signature[], checker: TypeChecker, context: TypeConstructionContext, enclosingDeclaration: ClassLikeDeclaration): TypeNode | undefined { @@ -406,7 +428,8 @@ namespace ts.codefix { typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], returnType: TypeNode | undefined, - quotePreference: QuotePreference + quotePreference: QuotePreference, + body: Block | undefined ): MethodDeclaration { return factory.createMethodDeclaration( /*decorators*/ undefined, @@ -417,7 +440,7 @@ namespace ts.codefix { typeParameters, parameters, returnType, - createStubbedMethodBody(quotePreference)); + body || createStubbedMethodBody(quotePreference)); } function createStubbedMethodBody(quotePreference: QuotePreference) { diff --git a/src/services/completions.ts b/src/services/completions.ts index 2f5f0323a9..c70efda3f3 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -244,7 +244,6 @@ namespace ts.Completions { // If the request is a continuation of an earlier `isIncomplete` response, // we can continue it from the cached previous response. - const typeChecker = program.getTypeChecker(); const compilerOptions = program.getCompilerOptions(); const incompleteCompletionsCache = preferences.allowIncompleteCompletions ? host.getIncompleteCompletionsCache?.() : undefined; if (incompleteCompletionsCache && completionKind === CompletionTriggerKind.TriggerForIncompleteCompletions && previousToken && isIdentifier(previousToken)) { @@ -257,7 +256,7 @@ namespace ts.Completions { incompleteCompletionsCache?.clear(); } - const stringCompletions = StringCompletions.getStringLiteralCompletions(sourceFile, position, previousToken, typeChecker, compilerOptions, host, log, preferences); + const stringCompletions = StringCompletions.getStringLiteralCompletions(sourceFile, position, previousToken, compilerOptions, host, program, log, preferences); if (stringCompletions) { return stringCompletions; } @@ -274,7 +273,7 @@ namespace ts.Completions { switch (completionData.kind) { case CompletionDataKind.Data: - const response = completionInfoFromData(sourceFile, typeChecker, compilerOptions, log, completionData, preferences); + const response = completionInfoFromData(sourceFile, host, program, compilerOptions, log, completionData, preferences); if (response?.isIncomplete) { incompleteCompletionsCache?.set(response); } @@ -403,7 +402,15 @@ namespace ts.Completions { return location?.kind === SyntaxKind.Identifier ? createTextSpanFromNode(location) : undefined; } - function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined { + function completionInfoFromData( + sourceFile: SourceFile, + host: LanguageServiceHost, + program: Program, + compilerOptions: CompilerOptions, + log: Log, + completionData: CompletionData, + preferences: UserPreferences, + ): CompletionInfo | undefined { const { symbols, contextToken, @@ -443,7 +450,8 @@ namespace ts.Completions { contextToken, location, sourceFile, - typeChecker, + host, + program, getEmitScriptTarget(compilerOptions), log, completionKind, @@ -472,7 +480,8 @@ namespace ts.Completions { contextToken, location, sourceFile, - typeChecker, + host, + program, getEmitScriptTarget(compilerOptions), log, completionKind, @@ -614,7 +623,8 @@ namespace ts.Completions { contextToken: Node | undefined, location: Node, sourceFile: SourceFile, - typeChecker: TypeChecker, + host: LanguageServiceHost, + program: Program, name: string, needsConvertPropertyAccess: boolean, origin: SymbolOriginInfo | undefined, @@ -625,6 +635,7 @@ namespace ts.Completions { useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences, + completionKind: CompletionKind, ): CompletionEntry | undefined { let insertText: string | undefined; let replacementSpan = getReplacementSpanForContextToken(replacementToken); @@ -633,6 +644,7 @@ namespace ts.Completions { let sourceDisplay; let hasAction; + const typeChecker = program.getTypeChecker(); const insertQuestionDot = origin && originIsNullableMember(origin); const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess; if (origin && originIsThisType(origin)) { @@ -686,13 +698,11 @@ namespace ts.Completions { } } - if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { - return undefined; - } - - if (originIsExport(origin) || originIsResolvedExport(origin)) { - data = originToCompletionEntryData(origin); - hasAction = !importCompletionNode; + if (preferences.includeCompletionsWithClassMemberSnippets && + preferences.includeCompletionsWithInsertText && + completionKind === CompletionKind.MemberLike && + isClassLikeMemberCompletion(symbol, location)) { + ({ insertText, isSnippet } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken)); } const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location); @@ -726,6 +736,15 @@ namespace ts.Completions { } } + if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { + return undefined; + } + + if (originIsExport(origin) || originIsResolvedExport(origin)) { + data = originToCompletionEntryData(origin); + hasAction = !importCompletionNode; + } + // TODO(drosen): Right now we just permit *all* semantic meanings when calling // 'getSymbolKind' which is permissible given that it is backwards compatible; but // really we should consider passing the meaning for the node so that we don't report @@ -752,8 +771,261 @@ namespace ts.Completions { }; } - function escapeSnippetText(text: string): string { - return text.replace(/\$/gm, "\\$"); + function isClassLikeMemberCompletion(symbol: Symbol, location: Node): boolean { + // TODO: support JS files. + if (isInJSFile(location)) { + return false; + } + + // Completion symbol must be for a class member. + const memberFlags = + SymbolFlags.ClassMember + & SymbolFlags.EnumMemberExcludes; + /* In + `class C { + | + }` + `location` is a class-like declaration. + In + `class C { + m| + }` + `location` is an identifier, + `location.parent` is a class element declaration, + and `location.parent.parent` is a class-like declaration. + In + `abstract class C { + abstract + abstract m| + }` + `location` is a syntax list (with modifiers as children), + and `location.parent` is a class-like declaration. + */ + return !!(symbol.flags & memberFlags) && + ( + isClassLike(location) || + ( + location.parent && + location.parent.parent && + isClassElement(location.parent) && + location === location.parent.name && + isClassLike(location.parent.parent) + ) || + ( + location.parent && + isSyntaxList(location) && + isClassLike(location.parent) + ) + ); + } + + function getEntryForMemberCompletion( + host: LanguageServiceHost, + program: Program, + options: CompilerOptions, + preferences: UserPreferences, + name: string, + symbol: Symbol, + location: Node, + contextToken: Node | undefined, + ): { insertText: string, isSnippet?: true } { + const classLikeDeclaration = findAncestor(location, isClassLike); + if (!classLikeDeclaration) { + return { insertText: name }; + } + + let isSnippet: true | undefined; + let insertText: string = name; + + const checker = program.getTypeChecker(); + const sourceFile = location.getSourceFile(); + const printer = createSnippetPrinter({ + removeComments: true, + module: options.module, + target: options.target, + omitTrailingSemicolon: true, + newLine: getNewLineKind(getNewLineCharacter(options, maybeBind(host, host.getNewLine))), + }); + const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); + + let body; + let tabstopStart = 1; + if (preferences.includeCompletionsWithSnippetText) { + isSnippet = true; + // We are adding a final tabstop (i.e. $0) in the body of the suggested member, if it has one. + // Note: this assumes we won't have more than one body in the completion nodes, which should be the case. + const emptyStatement1 = factory.createExpressionStatement(factory.createIdentifier("")); + setSnippetElement(emptyStatement1, { kind: SnippetKind.TabStop, order: 1 }); + tabstopStart = 2; + body = factory.createBlock([emptyStatement1], /* multiline */ true); + } + else { + body = factory.createBlock([], /* multiline */ true); + } + + let modifiers = ModifierFlags.None; + // Whether the suggested member should be abstract. + // e.g. in `abstract class C { abstract | }`, we should offer abstract method signatures at position `|`. + // Note: We are relying on checking if the context token is `abstract`, + // since other visibility modifiers (e.g. `protected`) should come *before* `abstract`. + // However, that is not true for the e.g. `override` modifier, so this check has its limitations. + const isAbstract = contextToken && isModifierLike(contextToken) === SyntaxKind.AbstractKeyword; + const completionNodes: Node[] = []; + codefix.addNewNodeForMemberSymbol( + symbol, + classLikeDeclaration, + sourceFile, + { program, host }, + preferences, + importAdder, + // `addNewNodeForMemberSymbol` calls this callback function for each new member node + // it adds for the given member symbol. + // We store these member nodes in the `completionNodes` array. + // Note: there might be: + // - No nodes if `addNewNodeForMemberSymbol` cannot figure out a node for the member; + // - One node; + // - More than one node if the member is overloaded (e.g. a method with overload signatures). + node => { + let requiredModifiers = ModifierFlags.None; + if (isAbstract) { + requiredModifiers |= ModifierFlags.Abstract; + } + if (isClassElement(node) + && checker.getMemberOverrideModifierStatus(classLikeDeclaration, node) === MemberOverrideStatus.NeedsOverride) { + requiredModifiers |= ModifierFlags.Override; + } + + let presentModifiers = ModifierFlags.None; + if (!completionNodes.length) { + // Omit already present modifiers from the first completion node/signature. + if (contextToken) { + presentModifiers = getPresentModifiers(contextToken); + } + // Keep track of added missing required modifiers and modifiers already present. + // This is needed when we have overloaded signatures, + // so this callback will be called for multiple nodes/signatures, + // and we need to make sure the modifiers are uniform for all nodes/signatures. + modifiers = node.modifierFlagsCache | requiredModifiers | presentModifiers; + } + node = factory.updateModifiers(node, modifiers & (~presentModifiers)); + + completionNodes.push(node); + }, + body, + isAbstract); + + if (completionNodes.length) { + if (preferences.includeCompletionsWithSnippetText) { + addSnippets(completionNodes, tabstopStart); + } + insertText = printer.printSnippetList(ListFormat.MultiLine, factory.createNodeArray(completionNodes), sourceFile); + } + + return { insertText, isSnippet }; + } + + function getPresentModifiers(contextToken: Node): ModifierFlags { + let modifiers = ModifierFlags.None; + let contextMod; + /* + Cases supported: + In + `class C { + public abstract | + }` + `contextToken` is ``abstract`` (as an identifier), + `contextToken.parent` is property declaration, + `location` is class declaration ``class C { ... }``. + In + `class C { + protected override m| + }` + `contextToken` is ``override`` (as a keyword), + `contextToken.parent` is property declaration, + `location` is identifier ``m``, + `location.parent` is property declaration ``protected override m``, + `location.parent.parent` is class declaration ``class C { ... }``. + */ + if (contextMod = isModifierLike(contextToken)) { + modifiers |= modifierToFlag(contextMod); + } + if (isPropertyDeclaration(contextToken.parent)) { + modifiers |= modifiersToFlags(contextToken.parent.modifiers); + } + return modifiers; + } + + function isModifierLike(node: Node): ModifierSyntaxKind | undefined { + if (isModifier(node)) { + return node.kind; + } + if (isIdentifier(node) && node.originalKeywordKind && isModifierKind(node.originalKeywordKind)) { + return node.originalKeywordKind; + } + return undefined; + } + + function addSnippets(nodes: Node[], orderStart: number): void { + let order = orderStart; + for (const node of nodes) { + addSnippetsWorker(node, /*parent*/ undefined); + } + + function addSnippetsWorker(node: Node, parent: Node | undefined) { + if (isVariableLike(node) && node.kind === SyntaxKind.Parameter) { + // Placeholder + setSnippetElement(node.name, { kind: SnippetKind.Placeholder, order }); + order += 1; + if (node.type) { + setSnippetElement(node.type, { kind: SnippetKind.Placeholder, order }); + order += 1; + } + } + else if (isTypeNode(node) && parent && isFunctionLikeDeclaration(parent)) { + setSnippetElement(node, { kind: SnippetKind.Placeholder, order }); + order += 1; + } + else if (isTypeParameterDeclaration(node) && parent && isFunctionLikeDeclaration(parent)) { + setSnippetElement(node, { kind: SnippetKind.Placeholder, order }); + order += 1; + } + + forEachChild(node, child => addSnippetsWorker(child, node)); + } + } + + function createSnippetPrinter( + printerOptions: PrinterOptions, + ) { + const printer = createPrinter(printerOptions); + const baseWriter = createTextWriter(getNewLineCharacter(printerOptions)); + const writer: EmitTextWriter = { + ...baseWriter, + write: s => baseWriter.write(escapeSnippetText(s)), + nonEscapingWrite: baseWriter.write, + writeLiteral: s => baseWriter.writeLiteral(escapeSnippetText(s)), + writeStringLiteral: s => baseWriter.writeStringLiteral(escapeSnippetText(s)), + writeSymbol: (s, symbol) => baseWriter.writeSymbol(escapeSnippetText(s), symbol), + writeParameter: s => baseWriter.writeParameter(escapeSnippetText(s)), + writeComment: s => baseWriter.writeComment(escapeSnippetText(s)), + writeProperty: s => baseWriter.writeProperty(escapeSnippetText(s)), + }; + + return { + printSnippetList, + }; + + + /* Snippet-escaping version of `printer.printList`. */ + function printSnippetList( + format: ListFormat, + list: NodeArray, + sourceFile: SourceFile | undefined, + ): string { + writer.clear(); + printer.writeList(format, list, sourceFile, writer); + return writer.getText(); + } } function originToCompletionEntryData(origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport): CompletionEntryData | undefined { @@ -863,7 +1135,8 @@ namespace ts.Completions { contextToken: Node | undefined, location: Node, sourceFile: SourceFile, - typeChecker: TypeChecker, + host: LanguageServiceHost, + program: Program, target: ScriptTarget, log: Log, kind: CompletionKind, @@ -881,6 +1154,7 @@ namespace ts.Completions { const start = timestamp(); const variableDeclaration = getVariableDeclaration(location); const useSemicolons = probablyUsesSemicolons(sourceFile); + const typeChecker = program.getTypeChecker(); // Tracks unique names. // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. @@ -904,7 +1178,8 @@ namespace ts.Completions { contextToken, location, sourceFile, - typeChecker, + host, + program, name, needsConvertPropertyAccess, origin, @@ -914,7 +1189,8 @@ namespace ts.Completions { importCompletionNode, useSemicolons, compilerOptions, - preferences + preferences, + kind, ); if (!entry) { continue; diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index 91c886b189..2c1a453df6 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -1,18 +1,35 @@ /* @internal */ namespace ts.Completions.StringCompletions { - export function getStringLiteralCompletions(sourceFile: SourceFile, position: number, contextToken: Node | undefined, checker: TypeChecker, options: CompilerOptions, host: LanguageServiceHost, log: Log, preferences: UserPreferences): CompletionInfo | undefined { + export function getStringLiteralCompletions( + sourceFile: SourceFile, + position: number, + contextToken: Node | undefined, + options: CompilerOptions, + host: LanguageServiceHost, + program: Program, + log: Log, + preferences: UserPreferences): CompletionInfo | undefined { if (isInReferenceComment(sourceFile, position)) { const entries = getTripleSlashReferenceCompletion(sourceFile, position, options, host); return entries && convertPathCompletions(entries); } if (isInString(sourceFile, position, contextToken)) { if (!contextToken || !isStringLiteralLike(contextToken)) return undefined; - const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host, preferences); - return convertStringLiteralCompletions(entries, contextToken, sourceFile, checker, log, options, preferences); + const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, program.getTypeChecker(), options, host, preferences); + return convertStringLiteralCompletions(entries, contextToken, sourceFile, host, program, log, options, preferences); } } - function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: StringLiteralLike, sourceFile: SourceFile, checker: TypeChecker, log: Log, options: CompilerOptions, preferences: UserPreferences): CompletionInfo | undefined { + function convertStringLiteralCompletions( + completion: StringLiteralCompletion | undefined, + contextToken: StringLiteralLike, + sourceFile: SourceFile, + host: LanguageServiceHost, + program: Program, + log: Log, + options: CompilerOptions, + preferences: UserPreferences, + ): CompletionInfo | undefined { if (completion === undefined) { return undefined; } @@ -30,7 +47,8 @@ namespace ts.Completions.StringCompletions { contextToken, sourceFile, sourceFile, - checker, + host, + program, ScriptTarget.ESNext, log, CompletionKind.String, diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 7b0365b343..d204bb0bc0 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -1037,7 +1037,7 @@ namespace ts.textChanges { /** Note: output node may be mutated input node. */ export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } { const writer = createWriter(newLineCharacter); - const newLine = newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed; + const newLine = getNewLineKind(newLineCharacter); createPrinter({ newLine, neverAsciiEscape: true, diff --git a/src/services/utilities.ts b/src/services/utilities.ts index b7e26ffecc..70ae2dba74 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3279,5 +3279,9 @@ namespace ts { return decisionFromFile ?? program.usesUriStyleNodeCoreModules; } + export function getNewLineKind(newLineCharacter: string): NewLineKind { + return newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed; + } + // #endregion } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 116be4316f..4c259d793d 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4042,6 +4042,7 @@ declare namespace ts { readonly includeCompletionsWithSnippetText?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; + readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ @@ -9537,6 +9538,13 @@ declare namespace ts.server.protocol { * values, with insertion text to replace preceding `.` tokens with `?.`. */ readonly includeAutomaticOptionalChainCompletions?: boolean; + /** + * If enabled, completions for class members (e.g. methods and properties) will include + * a whole declaration for the member. + * E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of + * `class A { foo }`. + */ + readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 56d764c664..1885ae3396 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4042,6 +4042,7 @@ declare namespace ts { readonly includeCompletionsWithSnippetText?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; + readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly allowIncompleteCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/tests/cases/fourslash/completionsOverridingMethod.ts b/tests/cases/fourslash/completionsOverridingMethod.ts new file mode 100644 index 0000000000..997dc0a887 --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod.ts @@ -0,0 +1,343 @@ +/// + +// @newline: LF +// @Filename: a.ts +// Case: Concrete class implements abstract method +////abstract class ABase { +//// abstract foo(param1: string, param2: boolean): Promise; +////} +//// +////class ASub extends ABase { +//// f/*a*/ +////} + +// @Filename: b.ts +// Case: Concrete class overrides concrete method +////class BBase { +//// foo(a: string, b: string): string { +//// return a + b; +//// } +////} +//// +////class BSub extends BBase { +//// f/*b*/ +////} + +// @Filename: c.ts +// Case: Multiple overrides, concrete class overrides concrete method +////class CBase { +//// foo(a: string | number): string { +//// return a + ""; +//// } +////} +//// +////class CSub extends CBase { +//// foo(a: string): string { +//// return add; +//// } +////} +//// +////class CSub2 extends CSub { +//// f/*c*/ +////} + +// @Filename: d.ts +// Case: Abstract class extends abstract class +////abstract class DBase { +//// abstract foo(a: string): string; +////} +//// +////abstract class DSub extends DBase { +//// f/*d*/ +////} + +// @Filename: e.ts +// Case: Class implements interface +////interface EBase { +//// foo(a: string): string; +////} +//// +////class ESub implements EBase { +//// f/*e*/ +////} + +// @Filename: f.ts +// Case: Abstract class implements interface +////interface FBase { +//// foo(a: string): string; +////} +//// +////abstract class FSub implements FBase { +//// f/*f*/ +////} + +// @Filename: g.ts +// Case: Method has overloads +////interface GBase { +//// foo(a: string): string; +//// foo(a: undefined, b: number): string; +////} +//// +////class GSub implements GBase { +//// f/*g*/ +////} + +// @Filename: h.ts +// Case: Static method +// Note: static methods are only suggested for completions after the `static` keyword +////class HBase { +//// static met(n: number): number { +//// return n; +//// } +////} +//// +////class HSub extends HBase { +//// /*h1*/ +//// static /*h2*/ +////} + +// @Filename: i.ts +// Case: Generic method +////class IBase { +//// met(t: T): T { +//// return t; +//// } +//// metcons(t: T): T { +//// return t; +//// } +////} +//// +////class ISub extends IBase { +//// /*i*/ +////} + + +verify.completions({ + marker: "a", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"foo(param1: string, param2: boolean): Promise {\n}\n", + } + ], +}); + +verify.completions({ + marker: "b", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"foo(a: string, b: string): string {\n}\n", + } + ], +}); + +verify.completions({ + marker: "c", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"foo(a: string): string {\n}\n", + } + ], +}); + +verify.completions({ + marker: "d", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"foo(a: string): string {\n}\n", + } + ], +}); + +verify.completions({ + marker: "e", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"foo(a: string): string {\n}\n", + } + ], +}); + +verify.completions({ + marker: "f", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"foo(a: string): string {\n}\n", + } + ], +}); + +verify.completions({ + marker: "g", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"foo(a: string): string;\n\ +foo(a: undefined, b: number): string;\n\ +foo(a: any, b?: any): string {\n}\n", + } + ], +}); + +verify.completions({ + marker: "h1", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + excludes: "met", +}); +verify.completions({ + marker: "h2", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "met", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"met(n: number): number {\n}\n", + } + ], +}); + +verify.completions({ + marker: "i", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "met", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"met(t: T): T {\n}\n", + }, + { + name: "metcons", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"metcons(t: T): T {\n}\n", + } + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsOverridingMethod1.ts b/tests/cases/fourslash/completionsOverridingMethod1.ts new file mode 100644 index 0000000000..395e4272d8 --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod1.ts @@ -0,0 +1,37 @@ +/// + +// @newline: LF +// @Filename: h.ts +// @noImplicitOverride: true +// Case: Suggested method needs `override` modifier +////class HBase { +//// foo(a: string): void {} +////} +//// +////class HSub extends HBase { +//// f/*h*/ +////} + + +verify.completions({ + marker: "h", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"override foo(a: string): void {\n}\n", + } + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsOverridingMethod2.ts b/tests/cases/fourslash/completionsOverridingMethod2.ts new file mode 100644 index 0000000000..724e958c55 --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod2.ts @@ -0,0 +1,36 @@ +/// + +// @newline: LF +// @Filename: a.ts +// Case: Snippet text needs escaping +////interface DollarSign { +//// "$usd"(a: number): number; +////} +////class USD implements DollarSign { +//// /*a*/ +////} + + +verify.completions({ + marker: "a", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: true, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "$usd", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + isSnippet: true, + insertText: +"\"\\$usd\"(${2:a}: ${3:number}): ${4:number} {\n $1\n}\n", + } + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsOverridingMethod3.ts b/tests/cases/fourslash/completionsOverridingMethod3.ts new file mode 100644 index 0000000000..1cb0bd6468 --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod3.ts @@ -0,0 +1,36 @@ +/// + +// @newline: LF +// @Filename: boo.d.ts +// Case: Declaration files +////interface Ghost { +//// boo(): string; +////} +//// +////declare class Poltergeist implements Ghost { +//// /*b*/ +////} + + +verify.completions({ + marker: "b", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "boo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"boo(): string;\n", + } + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsOverridingMethod4.ts b/tests/cases/fourslash/completionsOverridingMethod4.ts new file mode 100644 index 0000000000..35da54835f --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod4.ts @@ -0,0 +1,64 @@ +/// + +// @newline: LF +// @Filename: secret.ts +// Case: accessibility modifier inheritance +////class Secret { +//// #secret(): string { +//// return "secret"; +//// } +//// +//// private tell(): string { +//// return this.#secret(); +//// } +//// +//// protected hint(): string { +//// return "hint"; +//// } +//// +//// public refuse(): string { +//// return "no comments"; +//// } +////} +//// +////class Gossip extends Secret { +//// /* no telling secrets */ +//// /*a*/ +////} + + +verify.completions({ + marker: "a", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + excludes: [ + "tell", + "#secret", + ], + includes: [ + { + name: "hint", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "protected hint(): string {\n}\n", + }, + { + name: "refuse", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "public refuse(): string {\n}\n", + } + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsOverridingMethod5.ts b/tests/cases/fourslash/completionsOverridingMethod5.ts new file mode 100644 index 0000000000..80e7416019 --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod5.ts @@ -0,0 +1,116 @@ +/// + +// @newline: LF +// @Filename: a.ts +// Case: abstract methods +////abstract class Ab { +//// abstract met(n: string): void; +//// met2(n: number): void { +//// +//// } +////} +//// +////abstract class Abc extends Ab { +//// /*a*/ +//// abstract /*b*/ +//// abstract m/*c*/ +////} + + +verify.completions({ + marker: "a", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "met", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "met(n: string): void {\n}\n", + }, + { + name: "met2", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "met2(n: number): void {\n}\n", + } + ], +}); + +verify.completions({ + marker: "b", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "met", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "met(n: string): void;\n", + }, + { + name: "met2", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "met2(n: number): void;\n", + } + ], +}); + +verify.completions({ + marker: "c", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "met", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "met(n: string): void;\n", + }, + { + name: "met2", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "met2(n: number): void;\n", + } + ], +}); + + diff --git a/tests/cases/fourslash/completionsOverridingMethod6.ts b/tests/cases/fourslash/completionsOverridingMethod6.ts new file mode 100644 index 0000000000..d6828a6e30 --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod6.ts @@ -0,0 +1,96 @@ +/// + +// @Filename: a.ts +// @newline: LF +// Case: modifier inheritance/deduplication +////class A { +//// public method(): number { +//// return 0; +//// } +////} +//// +////abstract class B extends A { +//// public abstract /*b*/ +////} +//// +////class C extends A { +//// public override m/*a*/ +////} +//// +////interface D { +//// fun(a: number): number; +//// fun(a: undefined, b: string): number; +////} +//// +////class E implements D { +//// public f/*c*/ +////} + +verify.completions({ + marker: "a", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "method", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "method(): number {\n}\n", + }, + ], +}); + +verify.completions({ + marker: "b", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "method", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: "method(): number;\n", + }, + ], +}); + +verify.completions({ + marker: "c", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "fun", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: + "fun(a: number): number;\n\ +public fun(a: undefined, b: string): number;\n\ +public fun(a: any, b?: any): number {\n}\n", + }, + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsOverridingMethod7.ts b/tests/cases/fourslash/completionsOverridingMethod7.ts new file mode 100644 index 0000000000..f0e3b5df5a --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingMethod7.ts @@ -0,0 +1,38 @@ +/// + +// @Filename: a.ts +// @newline: LF +// Case: abstract overloads +////abstract class Base { +//// abstract M(t: T): void; +//// abstract M(t: T, x: number): void; +////} +//// +////abstract class Derived extends Base { +//// abstract /*a*/ +////} + +verify.completions({ + marker: "a", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "M", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +`M(t: T): void; +abstract M(t: T, x: number): void; +`, + }, + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsOverridingProperties.ts b/tests/cases/fourslash/completionsOverridingProperties.ts new file mode 100644 index 0000000000..b930255e28 --- /dev/null +++ b/tests/cases/fourslash/completionsOverridingProperties.ts @@ -0,0 +1,39 @@ +/// + +// @newline: LF +// @Filename: a.ts +// Case: Properties +////class Base { +//// protected foo: string = "bar"; +//// +////} +//// +////class Sub extends Base { +//// /*a*/ +////} + + + + +verify.completions({ + marker: "a", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: false, + includeCompletionsWithClassMemberSnippets: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + insertText: +"protected foo: string;\n", + } + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index d8bde1e571..25b98d2118 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -646,6 +646,7 @@ declare namespace FourSlashInterface { readonly includeCompletionsForImportStatements?: boolean; readonly includeCompletionsWithSnippetText?: boolean; readonly includeCompletionsWithInsertText?: boolean; + readonly includeCompletionsWithClassMemberSnippets?: boolean; readonly allowIncompleteCompletions?: boolean; /** @deprecated use `includeCompletionsWithInsertText` */ readonly includeInsertTextCompletions?: boolean; diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts index dc7463df4b..b8a5b18da8 100644 --- a/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts +++ b/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts @@ -84,6 +84,7 @@ verify.completions({ ], preferences: { jsxAttributeCompletionStyle: "auto", - includeCompletionsWithSnippetText: true + includeCompletionsWithSnippetText: true, + includeCompletionsWithInsertText: true, } }); \ No newline at end of file diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleBraces.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleBraces.ts index 051e1bf410..608c71787d 100644 --- a/tests/cases/fourslash/jsxAttributeCompletionStyleBraces.ts +++ b/tests/cases/fourslash/jsxAttributeCompletionStyleBraces.ts @@ -90,6 +90,7 @@ verify.completions({ ], preferences: { jsxAttributeCompletionStyle: "braces", - includeCompletionsWithSnippetText: true + includeCompletionsWithSnippetText: true, + includeCompletionsWithInsertText: true, } }); \ No newline at end of file