diff --git a/src/services/completions.ts b/src/services/completions.ts index d88e59d26b8..246aaa729e4 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -678,7 +678,7 @@ namespace ts.Completions { } } - if (isMethodOverrideCompletion(symbol, location)) { + if (isClassLikeMemberCompletion(symbol, location)) { ({ insertText, isSnippet } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location)); kindModifiers = SymbolDisplay.getSymbolModifiers(typeChecker, symbol); // >> TODO: remove `abstract` modifier from symbol? } @@ -720,11 +720,15 @@ namespace ts.Completions { } // >> TODO: Find better location for code - // >> TODO: update this to `isMemberCompletion`... ? - function isMethodOverrideCompletion(symbol: Symbol, location: Node): boolean { - return !!(symbol.flags & SymbolFlags.Method) && isPropertyDeclaration(location.parent); - // >> TODO: add more checks. e.g. the method suggestion could come from an interface the class implements, - // or some other possibilities? + function isClassLikeMemberCompletion(symbol: Symbol, location: Node): boolean { + const memberFlags = + // SymbolFlags.Method + // | SymbolFlags.Accessor + // | SymbolFlags.Property + SymbolFlags.ClassMember + & SymbolFlags.EnumMemberExcludes; + // >> TODO: Flags: Constructor? Signature? + return !!findAncestor(location, isClassLike) && !!(symbol.flags & memberFlags); } function getEntryForMemberCompletion( @@ -753,11 +757,14 @@ namespace ts.Completions { let body; if (preferences.includeCompletionsWithSnippetText) { isSnippet = true; - body = factory.createBlock([], /* multiline */ true); // TODO: add tabstop + const tabStopStatement = factory.createExpressionStatement(factory.createIdentifier("$1")); + body = factory.createBlock([tabStopStatement], /* multiline */ true); } else { body = factory.createBlock([], /* multiline */ true); } + + const completionNodes: Node[] = []; codefix.addNewNodeForMemberSymbol( symbol, classLikeDeclaration, @@ -766,15 +773,23 @@ namespace ts.Completions { preferences, importAdder, node => { + // `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 that 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). if (isClassDeclaration(classLikeDeclaration) && hasAbstractModifier(classLikeDeclaration)) { // Add `abstract` modifier node = factory.updateModifiers( node, concatenate([factory.createModifier(SyntaxKind.AbstractKeyword)], node.modifiers), ); + // >> TODO: we want to remove the body in more cases I think + // >> e.g. interfaces? if (isMethodDeclaration(node)) { // Remove method body - // >> TODO: maybe move this up, when creating the body above? node = factory.updateMethodDeclaration( node, node.decorators, @@ -789,10 +804,13 @@ namespace ts.Completions { ); } } - insertText = printer.printNode(EmitHint.Unspecified, node, sourceFile); + completionNodes.push(node); }, body); + if (completionNodes.length) { + insertText = printer.printList(ListFormat.MultiLine, factory.createNodeArray(completionNodes), sourceFile); + } return { insertText, isSnippet }; } diff --git a/tests/cases/fourslash/completionsOverridingMethod.ts b/tests/cases/fourslash/completionsOverridingMethod.ts index ca4717ea70e..e99153e3112 100644 --- a/tests/cases/fourslash/completionsOverridingMethod.ts +++ b/tests/cases/fourslash/completionsOverridingMethod.ts @@ -50,6 +50,37 @@ //// 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*/ +////} + // format.setFormatOptions({ // newLineCharacter: "\n", // }); @@ -73,7 +104,7 @@ verify.completions({ }, isSnippet: true, insertText: -"foo(param1: string, param2: boolean): Promise {\r\n}", +"foo(param1: string, param2: boolean): Promise {\r\n $1;\r\n}\r\n", } ], }); @@ -96,7 +127,7 @@ verify.completions({ }, isSnippet: true, insertText: -"foo(a: string, b: string): string {\r\n}", +"foo(a: string, b: string): string {\r\n $1;\r\n}\r\n", } ], }); @@ -119,7 +150,7 @@ verify.completions({ }, isSnippet: true, insertText: -"foo(a: string): string {\r\n}", +"foo(a: string): string {\r\n $1;\r\n}\r\n", } ], }); @@ -142,7 +173,78 @@ verify.completions({ }, isSnippet: true, insertText: -"abstract foo(a: string): string;", // Currently fails because no trailing semicolon +"abstract foo(a: string): string;\r\n", + } + ], +}); + +verify.completions({ + marker: "e", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + isSnippet: true, + insertText: +"foo(a: string): string {\r\n $1;\r\n}\r\n", + } + ], +}); + +verify.completions({ + marker: "f", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + isSnippet: true, + insertText: +"abstract foo(a: string): string;\r\n", + } + ], +}); + +verify.completions({ + marker: "g", + isNewIdentifierLocation: true, + preferences: { + includeCompletionsWithInsertText: true, + includeCompletionsWithSnippetText: true, + }, + includes: [ + { + name: "foo", + sortText: completion.SortText.LocationPriority, + replacementSpan: { + fileName: "", + pos: 0, + end: 0, + }, + isSnippet: true, + insertText: +"foo(a: string): string;\r\n\ +foo(a: undefined, b: number): string;\r\n\ +foo(a: any, b?: any): string {\r\n $1;\r\n}\r\n", } ], }); \ No newline at end of file