Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ import {
getSyntheticLeadingComments,
getSyntheticTrailingComments,
getTextOfIdentifierOrLiteral,
HasDecorators,
hasInvalidEscape,
HasModifiers,
hasProperty,
Expand Down Expand Up @@ -1023,6 +1024,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
liftToBlock,
mergeLexicalEnvironment,
updateModifiers,
updateModifierLike,
};

forEach(nodeFactoryPatchers, fn => fn(factory));
Expand Down Expand Up @@ -6989,6 +6991,18 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
Debug.assertNever(node);
}

function updateModifierLike<T extends HasModifiers & HasDecorators>(node: T, modifiers: readonly ModifierLike[]): T;
function updateModifierLike(node: HasModifiers & HasDecorators, modifierArray: readonly ModifierLike[]) {
return isParameter(node) ? updateParameterDeclaration(node, modifierArray, node.dotDotDotToken, node.name, node.questionToken, node.type, node.initializer) :
isPropertyDeclaration(node) ? updatePropertyDeclaration(node, modifierArray, node.name, node.questionToken ?? node.exclamationToken, node.type, node.initializer) :
isMethodDeclaration(node) ? updateMethodDeclaration(node, modifierArray, node.asteriskToken, node.name, node.questionToken, node.typeParameters, node.parameters, node.type, node.body) :
isGetAccessorDeclaration(node) ? updateGetAccessorDeclaration(node, modifierArray, node.name, node.parameters, node.type, node.body) :
isSetAccessorDeclaration(node) ? updateSetAccessorDeclaration(node, modifierArray, node.name, node.parameters, node.body) :
isClassExpression(node) ? updateClassExpression(node, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) :
isClassDeclaration(node) ? updateClassDeclaration(node, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) :
Debug.assertNever(node);
}

function asNodeArray<T extends Node>(array: readonly T[]): NodeArray<T>;
function asNodeArray<T extends Node>(array: readonly T[] | undefined): NodeArray<T> | undefined;
function asNodeArray<T extends Node>(array: readonly T[] | undefined): NodeArray<T> | undefined {
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1335,7 +1335,7 @@ export type HasIllegalDecorators =
;

// NOTE: Changing the following list requires changes to:
// - `canHaveModifiers` in factory/utilities.ts
// - `canHaveModifiers` in factory/utilitiesPublic.ts
// - `updateModifiers` in factory/nodeFactory.ts
export type HasModifiers =
| TypeParameterDeclaration
Expand Down Expand Up @@ -9054,6 +9054,7 @@ export interface NodeFactory {
*/
cloneNode<T extends Node | undefined>(node: T): T;
/** @internal */ updateModifiers<T extends HasModifiers>(node: T, modifiers: readonly Modifier[] | ModifierFlags | undefined): T;
/** @internal */ updateModifierLike<T extends HasModifiers & HasDecorators>(node: T, modifierLike: readonly ModifierLike[] | undefined): T;
}

/** @internal */
Expand Down
14 changes: 12 additions & 2 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,8 +1009,18 @@ export class TestState {
assert.equal<boolean | undefined>(actual.isPackageJsonImport, expected.isPackageJsonImport, `At entry ${actual.name}: Expected 'isPackageJsonImport' properties to match`);
}

assert.equal(actual.labelDetails?.description, expected.labelDetails?.description, `At entry ${actual.name}: Expected 'labelDetails.description' properties to match`);
assert.equal(actual.labelDetails?.detail, expected.labelDetails?.detail, `At entry ${actual.name}: Expected 'labelDetails.detail' properties to match`);
assert.equal(
actual.filterText,
expected.filterText,
`At entry ${actual.name}: Completion 'filterText' not match: ${showTextDiff(expected.filterText || "", actual.filterText || "")}`);
assert.equal(
actual.labelDetails?.description,
expected.labelDetails?.description,
`At entry ${actual.name}: Completion 'labelDetails.description' did not match: ${showTextDiff(expected.labelDetails?.description || "", actual.labelDetails?.description || "")}`);
assert.equal(
actual.labelDetails?.detail,
expected.labelDetails?.detail,
`At entry ${actual.name}: Completion 'labelDetails.detail' did not match: ${showTextDiff(expected.labelDetails?.detail || "", actual.labelDetails?.detail || "")}`);
assert.equal(actual.hasAction, expected.hasAction, `At entry ${actual.name}: Expected 'hasAction' properties to match`);
assert.equal(actual.isRecommended, expected.isRecommended, `At entry ${actual.name}: Expected 'isRecommended' properties to match'`);
assert.equal(actual.isSnippet, expected.isSnippet, `At entry ${actual.name}: Expected 'isSnippet' properties to match`);
Expand Down
1 change: 1 addition & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,7 @@ export interface ExpectedCompletionEntryObject {
readonly name: string;
readonly source?: string;
readonly insertText?: string;
readonly filterText?: string;
readonly replacementSpan?: FourSlash.Range;
readonly hasAction?: boolean; // If not specified, will assert that this is false.
readonly isRecommended?: boolean; // If not specified, will assert that this is false.
Expand Down
5 changes: 5 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2335,6 +2335,11 @@ export interface CompletionEntry {
* coupled with `replacementSpan` to replace a dotted access with a bracket access.
*/
insertText?: string;
/**
* A string that should be used when filtering a set of
* completion items.
*/
filterText?: string;
/**
* `insertText` should be interpreted as a snippet if true.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2267,6 +2267,7 @@ export class Session<TMessage = string> implements EventSender {
kindModifiers,
sortText,
insertText,
filterText,
replacementSpan,
hasAction,
source,
Expand All @@ -2285,6 +2286,7 @@ export class Session<TMessage = string> implements EventSender {
kindModifiers,
sortText,
insertText,
filterText,
replacementSpan: convertedSpan,
isSnippet,
hasAction: hasAction || undefined,
Expand Down
3 changes: 2 additions & 1 deletion src/services/codefixes/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ export function addNewNodeForMemberSymbol(
const kind = declaration?.kind ?? SyntaxKind.PropertySignature;
const declarationName = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
const effectiveModifierFlags = declaration ? getEffectiveModifierFlags(declaration) : ModifierFlags.None;
let modifierFlags =
let modifierFlags = effectiveModifierFlags & ModifierFlags.Static;
modifierFlags |=
effectiveModifierFlags & ModifierFlags.Public ? ModifierFlags.Public :
effectiveModifierFlags & ModifierFlags.Protected ? ModifierFlags.Protected :
ModifierFlags.None;
Expand Down
114 changes: 83 additions & 31 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
BindingPattern,
BreakOrContinueStatement,
CancellationToken,
canHaveDecorators,
canUsePropertyAccess,
CaseBlock,
cast,
Expand Down Expand Up @@ -44,6 +45,7 @@ import {
createTextSpanFromRange,
Debug,
Declaration,
Decorator,
Diagnostics,
diagnosticToString,
displayPart,
Expand Down Expand Up @@ -91,6 +93,7 @@ import {
getLineAndCharacterOfPosition,
getLineStartPositionForPosition,
getLocalSymbolForExportDefault,
getModifiers,
getNameOfDeclaration,
getNameTable,
getNewLineCharacter,
Expand Down Expand Up @@ -145,6 +148,7 @@ import {
isConstructorDeclaration,
isContextualKeyword,
isDeclarationName,
isDecorator,
isDeprecatedDeclaration,
isEntityName,
isEnumMember,
Expand Down Expand Up @@ -276,6 +280,7 @@ import {
memoizeOne,
MethodDeclaration,
ModifierFlags,
ModifierLike,
modifiersToFlags,
ModifierSyntaxKind,
modifierToFlag,
Expand Down Expand Up @@ -1609,6 +1614,7 @@ function createCompletionEntry(
includeSymbol: boolean
): CompletionEntry | undefined {
let insertText: string | undefined;
let filterText: string | undefined;
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
let data: CompletionEntryData | undefined;
let isSnippet: true | undefined;
Expand Down Expand Up @@ -1682,12 +1688,16 @@ function createCompletionEntry(
completionKind === CompletionKind.MemberLike &&
isClassLikeMemberCompletion(symbol, location, sourceFile)) {
let importAdder;
({ insertText, isSnippet, importAdder, replacementSpan } =
getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, position, contextToken, formatContext));
sortText = SortText.ClassMemberSnippets; // sortText has to be lower priority than the sortText for keywords. See #47852.
if (importAdder?.hasFixes()) {
hasAction = true;
source = CompletionSource.ClassMemberSnippet;
const memberCompletionEntry = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, position, contextToken, formatContext);
if (memberCompletionEntry) {
({ insertText, filterText, isSnippet, importAdder } = memberCompletionEntry);
if (importAdder?.hasFixes()) {
hasAction = true;
source = CompletionSource.ClassMemberSnippet;
}
}
else {
return undefined; // Skip this entry
}
}

Expand Down Expand Up @@ -1755,6 +1765,7 @@ function createCompletionEntry(
hasAction: hasAction ? true : undefined,
isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined,
insertText,
filterText,
replacementSpan,
sourceDisplay,
labelDetails,
Expand Down Expand Up @@ -1826,15 +1837,15 @@ function getEntryForMemberCompletion(
position: number,
contextToken: Node | undefined,
formatContext: formatting.FormatContext | undefined,
): { insertText: string, isSnippet?: true, importAdder?: codefix.ImportAdder, replacementSpan?: TextSpan } {
): { insertText: string, filterText?: string, isSnippet?: true, importAdder?: codefix.ImportAdder, eraseRange?: TextRange } | undefined {
const classLikeDeclaration = findAncestor(location, isClassLike);
if (!classLikeDeclaration) {
return { insertText: name };
return undefined; // This should never happen.
}

let isSnippet: true | undefined;
let replacementSpan: TextSpan | undefined;
let insertText: string = name;
const filterText: string = name;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If filter text is name why cant editors use name itself to sort.. may need to considerkind along with that but it feels like we are returning same data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The editor doesn't know that it should use name as the filterText, because in theory filterText could be anything. We could change the TS vscode extension to use the name as the filterText for class member snippet completions, but then that change would be restricted to vscode, and other editors would potentially run into the problem this PR is trying to fix.


const checker = program.getTypeChecker();
const sourceFile = location.getSourceFile();
Expand Down Expand Up @@ -1863,11 +1874,11 @@ function getEntryForMemberCompletion(
}

let modifiers = ModifierFlags.None;
const { modifiers: presentModifiers, range: eraseRange, decorators: presentDecorators } = getPresentModifiers(contextToken, sourceFile, position);
// Whether the suggested member should be abstract.
// e.g. in `abstract class C { abstract | }`, we should offer abstract method signatures at position `|`.
const { modifiers: presentModifiers, span: modifiersSpan } = getPresentModifiers(contextToken, sourceFile, position);
const isAbstract = !!(presentModifiers & ModifierFlags.Abstract);
const completionNodes: Node[] = [];
const isAbstract = presentModifiers & ModifierFlags.Abstract && classLikeDeclaration.modifierFlagsCache & ModifierFlags.Abstract;
let completionNodes: codefix.AddNode[] = [];
codefix.addNewNodeForMemberSymbol(
symbol,
classLikeDeclaration,
Expand Down Expand Up @@ -1896,20 +1907,49 @@ function getEntryForMemberCompletion(
// 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;
modifiers = node.modifierFlagsCache | requiredModifiers;
}
node = factory.updateModifiers(node, modifiers);
completionNodes.push(node);
},
body,
codefix.PreserveOptionalFlags.Property,
isAbstract);
!!isAbstract);

if (completionNodes.length) {
const isMethod = symbol.flags & SymbolFlags.Method;
let allowedModifiers = modifiers | ModifierFlags.Override | ModifierFlags.Public;
if (!isMethod) {
allowedModifiers |= ModifierFlags.Ambient | ModifierFlags.Readonly;
}
else {
allowedModifiers |= ModifierFlags.Async;
}
const allowedAndPresent = presentModifiers & allowedModifiers;
if (presentModifiers & (~allowedModifiers)) {
return undefined; // This completion entry will be filtered out.
}
// If the original member is protected, we allow it to change to public.
if (modifiers & ModifierFlags.Protected && allowedAndPresent & ModifierFlags.Public) {
modifiers &= ~ModifierFlags.Protected;
}
// `public` modifier is optional and can be dropped.
if (allowedAndPresent !== ModifierFlags.None && !(allowedAndPresent & ModifierFlags.Public)) {
modifiers &= ~ModifierFlags.Public;
}
modifiers |= allowedAndPresent;
completionNodes = completionNodes.map(node => factory.updateModifiers(node, modifiers));
// Add back the decorators that were already present.
if (presentDecorators?.length) {
const lastNode = completionNodes[completionNodes.length - 1];
if (canHaveDecorators(lastNode)) {
completionNodes[completionNodes.length - 1] = factory.updateModifierLike(lastNode, (presentDecorators as ModifierLike[]).concat(getModifiers(lastNode) || []));
}
}

const format = ListFormat.MultiLine | ListFormat.NoTrailingNewLine;
replacementSpan = modifiersSpan;
// If we have access to formatting settings, we print the nodes using the emitter,
// and then format the printed text.
// If we have access to formatting settings, we print the nodes using the emitter,
// and then format the printed text.
if (formatContext) {
insertText = printer.printAndFormatSnippetList(
format,
Expand All @@ -1925,21 +1965,22 @@ function getEntryForMemberCompletion(
}
}

return { insertText, isSnippet, importAdder, replacementSpan };
return { insertText, filterText, isSnippet, importAdder, eraseRange };
}

function getPresentModifiers(
contextToken: Node | undefined,
sourceFile: SourceFile,
position: number): { modifiers: ModifierFlags, span?: TextSpan } {
position: number): { modifiers: ModifierFlags, decorators?: Decorator[], range?: TextRange } {
if (!contextToken ||
getLineAndCharacterOfPosition(sourceFile, position).line
> getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line) {
return { modifiers: ModifierFlags.None };
}
let modifiers = ModifierFlags.None;
let span;
let decorators: Decorator[] | undefined;
let contextMod;
const range: TextRange = { pos: position, end: position };
/*
Cases supported:
In
Expand All @@ -1959,15 +2000,19 @@ function getPresentModifiers(
`location.parent` is property declaration ``protected override m``,
`location.parent.parent` is class declaration ``class C { ... }``.
*/
if (contextMod = isModifierLike(contextToken)) {
modifiers |= modifierToFlag(contextMod);
span = createTextSpanFromNode(contextToken);
}
if (isPropertyDeclaration(contextToken.parent)) {
if (isPropertyDeclaration(contextToken.parent) && contextToken.parent.modifiers) {
modifiers |= modifiersToFlags(contextToken.parent.modifiers) & ModifierFlags.Modifier;
span = createTextSpanFromNode(contextToken.parent);
decorators = contextToken.parent.modifiers.filter(isDecorator) || [];
range.pos = Math.min(range.pos, contextToken.parent.modifiers.pos);
}
if (contextMod = isModifierLike(contextToken)) {
const contextModifierFlag = modifierToFlag(contextMod);
if (!(modifiers & contextModifierFlag)) {
modifiers |= contextModifierFlag;
range.pos = Math.min(range.pos, contextToken.pos);
}
}
return { modifiers, span };
return { modifiers, decorators, range: range.pos !== position ? range : undefined };
}

function isModifierLike(node: Node): ModifierSyntaxKind | undefined {
Expand Down Expand Up @@ -2770,7 +2815,7 @@ function getCompletionEntryCodeActionsAndSourceDisplay(
}

if (source === CompletionSource.ClassMemberSnippet) {
const { importAdder } = getEntryForMemberCompletion(
const { importAdder, eraseRange } = getEntryForMemberCompletion(
host,
program,
compilerOptions,
Expand All @@ -2780,11 +2825,18 @@ function getCompletionEntryCodeActionsAndSourceDisplay(
location,
position,
contextToken,
formatContext);
if (importAdder) {
formatContext)!;
if (importAdder || eraseRange) {
const changes = textChanges.ChangeTracker.with(
{ host, formatContext, preferences },
importAdder.writeFixes);
tracker => {
if (importAdder) {
importAdder.writeFixes(tracker);
}
if (eraseRange) {
tracker.deleteRange(sourceFile, eraseRange);
}
});
return {
sourceDisplay: undefined,
codeActions: [{
Expand Down
1 change: 1 addition & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,7 @@ export interface CompletionEntry {
kindModifiers?: string; // see ScriptElementKindModifier, comma separated
sortText: string;
insertText?: string;
filterText?: string;
isSnippet?: true;
/**
* An optional span that indicates the text to be replaced by this completion item.
Expand Down
6 changes: 6 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,11 @@ declare namespace ts {
* coupled with `replacementSpan` to replace a dotted access with a bracket access.
*/
insertText?: string;
/**
* A string that should be used when filtering a set of
* completion items.
*/
filterText?: string;
/**
* `insertText` should be interpreted as a snippet if true.
*/
Expand Down Expand Up @@ -10801,6 +10806,7 @@ declare namespace ts {
kindModifiers?: string;
sortText: string;
insertText?: string;
filterText?: string;
isSnippet?: true;
/**
* An optional span that indicates the text to be replaced by this completion item.
Expand Down
Loading