Add method signature completions (#46370)

* prototype creation for method override completion snippet

* WIP: start using codefix `addNewNodeForMemberSymbol` to create method decl node

* update type of addNewNodeForMemberSymbol

* add more tests and support more cases

* add more tests and fix some details

* wip: more fixes and tests

* expose check override modifier in checker

* fix test

* WIP: add snippet support

* WIP: snippet support on emitter, adding snippets in completions

* make add snippets work with overloads (not synced)

* fix snippet adding

* rebase

* WIP: try to add snippet escaping in emitter

* support escaping in snippets

* small fixes; fixed tests

* more tests and fixes

* fix new tests

* fix modifier inheritance for overloads

* merge conflict fixes; remove comments

* throw error if setOptions is called but not implemented

* fix newline handling

* fix weird stuff

* fix tests

* fix more tests

* Fix unbound host.getNewLine

* fix isParameterDeclaration changes

* rename diagnostic to status and remove snippets from public api

* rename emitter functions + fix indentation

* check completion kind before calling isclasslikemembercompletion

* fix missing type parameters

* Revert "fix missing type parameters"

This reverts commit 7bdeaa86da.

* add isAmbient flag to addNewNodeForMemberSymbol

* add test for abstract overloads

* refactor snippet escaping support

* add user preference flag for enabling class member snippets

* update API baseline

* update tabstop order

Co-authored-by: Andrew Branch <andrewbranch@users.noreply.github.com>
This commit is contained in:
Gabriela Araujo Britto 2021-10-28 16:05:36 -07:00 committed by GitHub
parent 566758d6cb
commit fd620c93f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1456 additions and 80 deletions

View file

@ -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

View file

@ -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);
}

View file

@ -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);

View file

@ -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<T extends Node>(node: T, snippet: SnippetElement): T {
const emitNode = getOrCreateEmitNode(node);
emitNode.snippetElement = snippet;
return node;
}
/* @internal */
export function ignoreSourceNewlines<T extends Node>(node: T): T {
getOrCreateEmitNode(node).flags |= EmitFlags.IgnoreSourceNewlines;

View file

@ -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" */

View file

@ -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, "\\$");
}
}

View file

@ -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" */

View file

@ -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);
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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<Node>,
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;

View file

@ -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,

View file

@ -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,

View file

@ -3279,5 +3279,9 @@ namespace ts {
return decisionFromFile ?? program.usesUriStyleNodeCoreModules;
}
export function getNewLineKind(newLineCharacter: string): NewLineKind {
return newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed;
}
// #endregion
}

View file

@ -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" */

View file

@ -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" */

View file

@ -0,0 +1,343 @@
/// <reference path="fourslash.ts" />
// @newline: LF
// @Filename: a.ts
// Case: Concrete class implements abstract method
////abstract class ABase {
//// abstract foo(param1: string, param2: boolean): Promise<void>;
////}
////
////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): T {
//// return t;
//// }
//// metcons<T extends string | number>(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<void> {\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): T {\n}\n",
},
{
name: "metcons",
sortText: completion.SortText.LocationPriority,
replacementSpan: {
fileName: "",
pos: 0,
end: 0,
},
insertText:
"metcons<T extends string | number>(t: T): T {\n}\n",
}
],
});

View file

@ -0,0 +1,37 @@
/// <reference path="fourslash.ts" />
// @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",
}
],
});

View file

@ -0,0 +1,36 @@
/// <reference path="fourslash.ts" />
// @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",
}
],
});

View file

@ -0,0 +1,36 @@
/// <reference path="fourslash.ts" />
// @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",
}
],
});

View file

@ -0,0 +1,64 @@
/// <reference path="fourslash.ts" />
// @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",
}
],
});

View file

@ -0,0 +1,116 @@
/// <reference path="fourslash.ts" />
// @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",
}
],
});

View file

@ -0,0 +1,96 @@
/// <reference path="fourslash.ts" />
// @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",
},
],
});

View file

@ -0,0 +1,38 @@
/// <reference path="fourslash.ts" />
// @Filename: a.ts
// @newline: LF
// Case: abstract overloads
////abstract class Base {
//// abstract M<T>(t: T): void;
//// abstract M<T>(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: T): void;
abstract M<T>(t: T, x: number): void;
`,
},
],
});

View file

@ -0,0 +1,39 @@
/// <reference path="fourslash.ts" />
// @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",
}
],
});

View file

@ -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;

View file

@ -84,6 +84,7 @@ verify.completions({
],
preferences: {
jsxAttributeCompletionStyle: "auto",
includeCompletionsWithSnippetText: true
includeCompletionsWithSnippetText: true,
includeCompletionsWithInsertText: true,
}
});

View file

@ -90,6 +90,7 @@ verify.completions({
],
preferences: {
jsxAttributeCompletionStyle: "braces",
includeCompletionsWithSnippetText: true
includeCompletionsWithSnippetText: true,
includeCompletionsWithInsertText: true,
}
});