mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Improve class member snippet completions and keyword completions interaction (#52525)
This commit is contained in:
@@ -125,6 +125,7 @@ import {
|
||||
getSyntheticLeadingComments,
|
||||
getSyntheticTrailingComments,
|
||||
getTextOfIdentifierOrLiteral,
|
||||
HasDecorators,
|
||||
hasInvalidEscape,
|
||||
HasModifiers,
|
||||
hasProperty,
|
||||
@@ -1023,6 +1024,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
|
||||
liftToBlock,
|
||||
mergeLexicalEnvironment,
|
||||
updateModifiers,
|
||||
updateModifierLike,
|
||||
};
|
||||
|
||||
forEach(nodeFactoryPatchers, fn => fn(factory));
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -9057,6 +9057,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 */
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -2267,6 +2267,7 @@ export class Session<TMessage = string> implements EventSender {
|
||||
kindModifiers,
|
||||
sortText,
|
||||
insertText,
|
||||
filterText,
|
||||
replacementSpan,
|
||||
hasAction,
|
||||
source,
|
||||
@@ -2285,6 +2286,7 @@ export class Session<TMessage = string> implements EventSender {
|
||||
kindModifiers,
|
||||
sortText,
|
||||
insertText,
|
||||
filterText,
|
||||
replacementSpan: convertedSpan,
|
||||
isSnippet,
|
||||
hasAction: hasAction || undefined,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+83
-31
@@ -7,6 +7,7 @@ import {
|
||||
BindingPattern,
|
||||
BreakOrContinueStatement,
|
||||
CancellationToken,
|
||||
canHaveDecorators,
|
||||
canUsePropertyAccess,
|
||||
CaseBlock,
|
||||
cast,
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
createTextSpanFromRange,
|
||||
Debug,
|
||||
Declaration,
|
||||
Decorator,
|
||||
Diagnostics,
|
||||
diagnosticToString,
|
||||
displayPart,
|
||||
@@ -91,6 +93,7 @@ import {
|
||||
getLineAndCharacterOfPosition,
|
||||
getLineStartPositionForPosition,
|
||||
getLocalSymbolForExportDefault,
|
||||
getModifiers,
|
||||
getNameOfDeclaration,
|
||||
getNameTable,
|
||||
getNewLineCharacter,
|
||||
@@ -145,6 +148,7 @@ import {
|
||||
isConstructorDeclaration,
|
||||
isContextualKeyword,
|
||||
isDeclarationName,
|
||||
isDecorator,
|
||||
isDeprecatedDeclaration,
|
||||
isEntityName,
|
||||
isEnumMember,
|
||||
@@ -276,6 +280,7 @@ import {
|
||||
memoizeOne,
|
||||
MethodDeclaration,
|
||||
ModifierFlags,
|
||||
ModifierLike,
|
||||
modifiersToFlags,
|
||||
ModifierSyntaxKind,
|
||||
modifierToFlag,
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1755,6 +1765,7 @@ function createCompletionEntry(
|
||||
hasAction: hasAction ? true : undefined,
|
||||
isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined,
|
||||
insertText,
|
||||
filterText,
|
||||
replacementSpan,
|
||||
sourceDisplay,
|
||||
labelDetails,
|
||||
@@ -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;
|
||||
|
||||
const checker = program.getTypeChecker();
|
||||
const sourceFile = location.getSourceFile();
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
return { modifiers, span };
|
||||
if (contextMod = isModifierLike(contextToken)) {
|
||||
const contextModifierFlag = modifierToFlag(contextMod);
|
||||
if (!(modifiers & contextModifierFlag)) {
|
||||
modifiers |= contextModifierFlag;
|
||||
range.pos = Math.min(range.pos, contextToken.pos);
|
||||
}
|
||||
}
|
||||
return { modifiers, decorators, range: range.pos !== position ? range : undefined };
|
||||
}
|
||||
|
||||
function isModifierLike(node: Node): ModifierSyntaxKind | undefined {
|
||||
@@ -2770,7 +2815,7 @@ function getCompletionEntryCodeActionsAndSourceDisplay(
|
||||
}
|
||||
|
||||
if (source === CompletionSource.ClassMemberSnippet) {
|
||||
const { importAdder } = getEntryForMemberCompletion(
|
||||
const { importAdder, eraseRange } = getEntryForMemberCompletion(
|
||||
host,
|
||||
program,
|
||||
compilerOptions,
|
||||
@@ -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: [{
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user