Skip to content

Commit 13460ae

Browse files
Fix(52604): Provide Object member completions without comma; insert a comma (#52899)
Co-authored-by: Daniel Rosenwasser <[email protected]>
1 parent c58cc39 commit 13460ae

13 files changed

+704
-5
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7684,6 +7684,10 @@
76847684
"category": "Message",
76857685
"code": 95186
76867686
},
7687+
"Add missing comma for object member completion '{0}'.": {
7688+
"category": "Message",
7689+
"code": 95187
7690+
},
76877691

76887692
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
76897693
"category": "Error",

src/services/completions.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ import {
164164
isFunctionLikeDeclaration,
165165
isFunctionLikeKind,
166166
isFunctionTypeNode,
167+
isGetAccessorDeclaration,
167168
isIdentifier,
168169
isIdentifierText,
169170
isImportableFile,
@@ -217,9 +218,11 @@ import {
217218
isPrivateIdentifier,
218219
isPrivateIdentifierClassElementDeclaration,
219220
isPropertyAccessExpression,
221+
isPropertyAssignment,
220222
isPropertyDeclaration,
221223
isPropertyNameLiteral,
222224
isRegularExpressionLiteral,
225+
isSetAccessorDeclaration,
223226
isShorthandPropertyAssignment,
224227
isSingleOrDoubleQuote,
225228
isSourceFile,
@@ -443,6 +446,8 @@ export enum CompletionSource {
443446
ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/",
444447
/** Case completions for switch statements */
445448
SwitchCases = "SwitchCases/",
449+
/** Completions for an Object literal expression */
450+
ObjectLiteralMemberWithComma = "ObjectLiteralMemberWithComma/",
446451
}
447452

448453
/** @internal */
@@ -1683,6 +1688,30 @@ function createCompletionEntry(
16831688
hasAction = true;
16841689
}
16851690

1691+
// Provide object member completions when missing commas, and insert missing commas.
1692+
// For example:
1693+
//
1694+
// interface I {
1695+
// a: string;
1696+
// b: number
1697+
// }
1698+
//
1699+
// const cc: I = { a: "red" | }
1700+
//
1701+
// Completion should add a comma after "red" and provide completions for b
1702+
if (completionKind === CompletionKind.ObjectPropertyDeclaration && contextToken && findPrecedingToken(contextToken.pos, sourceFile, contextToken)?.kind !== SyntaxKind.CommaToken) {
1703+
if (isMethodDeclaration(contextToken.parent.parent) ||
1704+
isGetAccessorDeclaration(contextToken.parent.parent) ||
1705+
isSetAccessorDeclaration(contextToken.parent.parent) ||
1706+
isSpreadAssignment(contextToken.parent) ||
1707+
findAncestor(contextToken.parent, isPropertyAssignment)?.getLastToken(sourceFile) === contextToken ||
1708+
isShorthandPropertyAssignment(contextToken.parent) && getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line !== getLineAndCharacterOfPosition(sourceFile, position).line) {
1709+
1710+
source = CompletionSource.ObjectLiteralMemberWithComma;
1711+
hasAction = true;
1712+
}
1713+
}
1714+
16861715
if (preferences.includeCompletionsWithClassMemberSnippets &&
16871716
preferences.includeCompletionsWithInsertText &&
16881717
completionKind === CompletionKind.MemberLike &&
@@ -2664,7 +2693,8 @@ function getSymbolCompletionFromEntryId(
26642693
return info && info.name === entryId.name && (
26652694
entryId.source === CompletionSource.ClassMemberSnippet && symbol.flags & SymbolFlags.ClassMember
26662695
|| entryId.source === CompletionSource.ObjectLiteralMethodSnippet && symbol.flags & (SymbolFlags.Property | SymbolFlags.Method)
2667-
|| getSourceFromOrigin(origin) === entryId.source)
2696+
|| getSourceFromOrigin(origin) === entryId.source
2697+
|| entryId.source === CompletionSource.ObjectLiteralMemberWithComma)
26682698
? { type: "symbol" as const, symbol, location, origin, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation }
26692699
: undefined;
26702700
}) || { type: "none" };
@@ -2860,6 +2890,23 @@ function getCompletionEntryCodeActionsAndSourceDisplay(
28602890
return { codeActions: [codeAction], sourceDisplay: undefined };
28612891
}
28622892

2893+
if (source === CompletionSource.ObjectLiteralMemberWithComma && contextToken) {
2894+
const changes = textChanges.ChangeTracker.with(
2895+
{ host, formatContext, preferences },
2896+
tracker => tracker.insertText(sourceFile, contextToken.end, ",")
2897+
);
2898+
2899+
if (changes) {
2900+
return {
2901+
sourceDisplay: undefined,
2902+
codeActions: [{
2903+
changes,
2904+
description: diagnosticToString([Diagnostics.Add_missing_comma_for_object_member_completion_0, name]),
2905+
}],
2906+
};
2907+
}
2908+
}
2909+
28632910
if (!origin || !(originIsExport(origin) || originIsResolvedExport(origin))) {
28642911
return { codeActions: undefined, sourceDisplay: undefined };
28652912
}
@@ -4156,7 +4203,7 @@ function getCompletionData(
41564203
*/
41574204
function tryGetObjectLikeCompletionSymbols(): GlobalsSearch | undefined {
41584205
const symbolsStartIndex = symbols.length;
4159-
const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken);
4206+
const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken, position, sourceFile);
41604207
if (!objectLikeContainer) return GlobalsSearch.Continue;
41614208

41624209
// We're looking up possible property names from contextual/inferred/declared type.
@@ -4884,7 +4931,7 @@ function getCompletionData(
48844931
* Returns the immediate owning object literal or binding pattern of a context token,
48854932
* on the condition that one exists and that the context implies completion should be given.
48864933
*/
4887-
function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined): ObjectLiteralExpression | ObjectBindingPattern | undefined {
4934+
function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined, position: number, sourceFile: SourceFile): ObjectLiteralExpression | ObjectBindingPattern | undefined {
48884935
if (contextToken) {
48894936
const { parent } = contextToken;
48904937
switch (contextToken.kind) {
@@ -4899,8 +4946,33 @@ function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined): Ob
48994946
case SyntaxKind.AsyncKeyword:
49004947
return tryCast(parent.parent, isObjectLiteralExpression);
49014948
case SyntaxKind.Identifier:
4902-
return (contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent)
4903-
? contextToken.parent.parent : undefined;
4949+
if ((contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent)) {
4950+
return contextToken.parent.parent;
4951+
}
4952+
else {
4953+
if (isObjectLiteralExpression(contextToken.parent.parent) &&
4954+
(isSpreadAssignment(contextToken.parent) || isShorthandPropertyAssignment(contextToken.parent) &&
4955+
(getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line !== getLineAndCharacterOfPosition(sourceFile, position).line))) {
4956+
return contextToken.parent.parent;
4957+
}
4958+
const ancestorNode = findAncestor(parent, isPropertyAssignment);
4959+
if (ancestorNode?.getLastToken(sourceFile) === contextToken && isObjectLiteralExpression(ancestorNode.parent)) {
4960+
return ancestorNode.parent;
4961+
}
4962+
}
4963+
break;
4964+
default:
4965+
if (parent.parent?.parent && (isMethodDeclaration(parent.parent) || isGetAccessorDeclaration(parent.parent) || isSetAccessorDeclaration(parent.parent)) && isObjectLiteralExpression(parent.parent.parent)) {
4966+
return parent.parent.parent;
4967+
}
4968+
if (isSpreadAssignment(parent) && isObjectLiteralExpression(parent.parent)) {
4969+
return parent.parent;
4970+
}
4971+
const ancestorNode = findAncestor(parent, isPropertyAssignment);
4972+
if (contextToken.kind !== SyntaxKind.ColonToken && ancestorNode?.getLastToken(sourceFile) === contextToken &&
4973+
isObjectLiteralExpression(ancestorNode.parent)) {
4974+
return ancestorNode.parent;
4975+
}
49044976
}
49054977
}
49064978

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/// <reference path="fourslash.ts" />
2+
//// interface ColorPalette {
3+
//// primary?: string;
4+
//// secondary?: string;
5+
//// }
6+
7+
//// let colors: ColorPalette = {
8+
//// primary: "red"
9+
//// /**/
10+
//// };
11+
12+
verify.completions({
13+
marker: "",
14+
includes: [{
15+
name: "secondary",
16+
sortText: completion.SortText.OptionalMember,
17+
hasAction: true,
18+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
19+
}],
20+
preferences: {
21+
allowIncompleteCompletions: true,
22+
includeInsertTextCompletions: true,
23+
},
24+
});
25+
26+
verify.applyCodeActionFromCompletion("", {
27+
name: "secondary",
28+
description: `Add missing comma for object member completion 'secondary'.`,
29+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
30+
newFileContent:
31+
`interface ColorPalette {
32+
primary?: string;
33+
secondary?: string;
34+
}
35+
let colors: ColorPalette = {
36+
primary: "red",
37+
38+
};`,
39+
preferences: {
40+
allowIncompleteCompletions: true,
41+
includeInsertTextCompletions: true,
42+
},
43+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/// <reference path="fourslash.ts" />
2+
//// interface TTTT {
3+
//// aaa: string,
4+
//// bbb?: number
5+
//// }
6+
//// const uuu: TTTT = {
7+
//// get aaa() {
8+
//// return ""
9+
//// }
10+
//// /**/
11+
//// }
12+
13+
verify.completions({
14+
marker: "",
15+
includes: [{
16+
name: "bbb",
17+
sortText: completion.SortText.OptionalMember,
18+
hasAction: true,
19+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
20+
}],
21+
preferences: {
22+
allowIncompleteCompletions: true,
23+
includeInsertTextCompletions: true,
24+
},
25+
});
26+
27+
verify.applyCodeActionFromCompletion("", {
28+
name: "bbb",
29+
description: `Add missing comma for object member completion 'bbb'.`,
30+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
31+
newFileContent:
32+
`interface TTTT {
33+
aaa: string,
34+
bbb?: number
35+
}
36+
const uuu: TTTT = {
37+
get aaa() {
38+
return ""
39+
},
40+
41+
}`,
42+
preferences: {
43+
allowIncompleteCompletions: true,
44+
includeInsertTextCompletions: true,
45+
},
46+
});
47+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/// <reference path="fourslash.ts" />
2+
//// interface ColorPalette {
3+
//// primary?: string;
4+
//// secondary?: string;
5+
//// }
6+
7+
//// interface I {
8+
//// color: ColorPalette;
9+
//// }
10+
11+
//// const a: I = {
12+
//// color: {primary: "red" /**/}
13+
//// }
14+
15+
verify.completions({
16+
marker: "",
17+
includes: [{
18+
name: "secondary",
19+
sortText: completion.SortText.OptionalMember,
20+
hasAction: true,
21+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
22+
}],
23+
preferences: {
24+
allowIncompleteCompletions: true,
25+
includeInsertTextCompletions: true,
26+
}
27+
});
28+
29+
verify.applyCodeActionFromCompletion("", {
30+
name: "secondary",
31+
description: `Add missing comma for object member completion 'secondary'.`,
32+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
33+
newFileContent:
34+
`interface ColorPalette {
35+
primary?: string;
36+
secondary?: string;
37+
}
38+
interface I {
39+
color: ColorPalette;
40+
}
41+
const a: I = {
42+
color: {primary: "red", }
43+
}`,
44+
preferences: {
45+
allowIncompleteCompletions: true,
46+
includeInsertTextCompletions: true,
47+
},
48+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/// <reference path="fourslash.ts" />
2+
////interface T {
3+
//// aaa?: string;
4+
//// foo(): void;
5+
//// }
6+
//// const obj: T = {
7+
//// foo() {
8+
//
9+
//// }
10+
//// /**/
11+
//// }
12+
13+
verify.completions({
14+
marker: "",
15+
includes: [{
16+
name: "aaa",
17+
sortText: completion.SortText.OptionalMember,
18+
hasAction: true,
19+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
20+
}],
21+
preferences: {
22+
allowIncompleteCompletions: true,
23+
includeInsertTextCompletions: true,
24+
},
25+
});
26+
27+
verify.applyCodeActionFromCompletion("", {
28+
name: "aaa",
29+
description: `Add missing comma for object member completion 'aaa'.`,
30+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
31+
newFileContent:
32+
`interface T {
33+
aaa?: string;
34+
foo(): void;
35+
}
36+
const obj: T = {
37+
foo() {
38+
},
39+
40+
}`,
41+
preferences: {
42+
allowIncompleteCompletions: true,
43+
includeInsertTextCompletions: true,
44+
},
45+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/// <reference path="fourslash.ts" />
2+
////interface T {
3+
//// aaa: number;
4+
//// bbb?: number;
5+
//// }
6+
//// const obj: T = {
7+
//// aaa: 1 * (2 + 3)
8+
//// /**/
9+
//// }
10+
11+
verify.completions({
12+
marker: "",
13+
includes: [{
14+
name: "bbb",
15+
sortText: completion.SortText.OptionalMember,
16+
hasAction: true,
17+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
18+
}],
19+
preferences: {
20+
allowIncompleteCompletions: true,
21+
includeInsertTextCompletions: true,
22+
},
23+
});
24+
25+
verify.applyCodeActionFromCompletion("", {
26+
name: "bbb",
27+
description: `Add missing comma for object member completion 'bbb'.`,
28+
source: completion.CompletionSource.ObjectLiteralMemberWithComma,
29+
newFileContent:
30+
`interface T {
31+
aaa: number;
32+
bbb?: number;
33+
}
34+
const obj: T = {
35+
aaa: 1 * (2 + 3),
36+
37+
}`,
38+
preferences: {
39+
allowIncompleteCompletions: true,
40+
includeInsertTextCompletions: true,
41+
},
42+
});

0 commit comments

Comments
 (0)