-
Notifications
You must be signed in to change notification settings - Fork 13.4k
Fix(52604): Provide Object member completions without comma; insert a comma #52899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
7c72784
fa517aa
6613dca
4f88134
586a2e5
3ab385f
abb5c07
c8c1362
463a435
54456cc
87b09f8
2fff618
b33aef8
731222c
89d178b
790b30c
c477b17
7e861b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -217,6 +217,7 @@ import { | |||||||
| isPrivateIdentifier, | ||||||||
| isPrivateIdentifierClassElementDeclaration, | ||||||||
| isPropertyAccessExpression, | ||||||||
| isPropertyAssignment, | ||||||||
| isPropertyDeclaration, | ||||||||
| isPropertyNameLiteral, | ||||||||
| isRegularExpressionLiteral, | ||||||||
|
|
@@ -443,6 +444,8 @@ export enum CompletionSource { | |||||||
| ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/", | ||||||||
| /** Case completions for switch statements */ | ||||||||
| SwitchCases = "SwitchCases/", | ||||||||
| /** Completions for an Object literal expression */ | ||||||||
| ObjectLiteralMemberWithComma = "ObjectLiteralMemberWithComma/", | ||||||||
| } | ||||||||
|
|
||||||||
| /** @internal */ | ||||||||
|
|
@@ -1683,6 +1686,15 @@ function createCompletionEntry( | |||||||
| hasAction = true; | ||||||||
| } | ||||||||
|
|
||||||||
| if (completionKind === CompletionKind.ObjectPropertyDeclaration && contextToken && | ||||||||
| findPrecedingToken(contextToken.pos, sourceFile, contextToken)?.kind !== SyntaxKind.CommaToken && | ||||||||
| (isMethodDeclaration(contextToken.parent.parent) || isSpreadAssignment(contextToken.parent) || findAncestor(contextToken.parent, (node: Node) => isPropertyAssignment(node))?.getLastToken() === contextToken || | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd also rather you just broke this into 2 nested if (completionKind === CompletionKind.ObjectPropertyDeclaration && contextToken && findPrecedingToken(contextToken.pos, sourceFile, contextToken)?.kind !== SyntaxKind.CommaToken) {
if (isMethodDeclaration(contextToken.parent.parent) ||
isSpreadAssignment(contextToken.parent) ||
...) {
source = CompletionSource.ObjectLiteralMemberWithComma;
hasAction = true;
}
}
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait, you don't need
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the spread assignment case should be handled the same as the property assignment case, because the spread assignment can also contain any expression, like this: const v: I = {
...a.b.c.d
}so I think the condition here should be something like: |
||||||||
| isShorthandPropertyAssignment(contextToken.parent) && getLineAndCharacterOfPosition(contextToken.getSourceFile(), contextToken.getEnd()).line !== getLineAndCharacterOfPosition(contextToken.getSourceFile(), position).line)) { | ||||||||
|
|
||||||||
| source = CompletionSource.ObjectLiteralMemberWithComma; | ||||||||
| hasAction = true; | ||||||||
| } | ||||||||
|
|
||||||||
| if (preferences.includeCompletionsWithClassMemberSnippets && | ||||||||
| preferences.includeCompletionsWithInsertText && | ||||||||
| completionKind === CompletionKind.MemberLike && | ||||||||
|
|
@@ -2664,7 +2676,8 @@ function getSymbolCompletionFromEntryId( | |||||||
| return info && info.name === entryId.name && ( | ||||||||
| entryId.source === CompletionSource.ClassMemberSnippet && symbol.flags & SymbolFlags.ClassMember | ||||||||
| || entryId.source === CompletionSource.ObjectLiteralMethodSnippet && symbol.flags & (SymbolFlags.Property | SymbolFlags.Method) | ||||||||
| || getSourceFromOrigin(origin) === entryId.source) | ||||||||
| || getSourceFromOrigin(origin) === entryId.source | ||||||||
| || entryId.source === CompletionSource.ObjectLiteralMemberWithComma) | ||||||||
| ? { type: "symbol" as const, symbol, location, origin, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } | ||||||||
| : undefined; | ||||||||
| }) || { type: "none" }; | ||||||||
|
|
@@ -2860,6 +2873,21 @@ function getCompletionEntryCodeActionsAndSourceDisplay( | |||||||
| return { codeActions: [codeAction], sourceDisplay: undefined }; | ||||||||
| } | ||||||||
|
|
||||||||
| if (source === CompletionSource.ObjectLiteralMemberWithComma && contextToken) { | ||||||||
| const changes = textChanges.ChangeTracker.with( | ||||||||
| { host, formatContext, preferences }, | ||||||||
| tracker => tracker.insertText(sourceFile, contextToken.end,",")); | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| if (changes) { | ||||||||
| return { | ||||||||
| sourceDisplay: undefined, | ||||||||
| codeActions: [{ | ||||||||
| changes, | ||||||||
| description: diagnosticToString([Diagnostics.Add_missing_comma_for_object_member_completion_0, name]), | ||||||||
| }], | ||||||||
| }; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if (!origin || !(originIsExport(origin) || originIsResolvedExport(origin))) { | ||||||||
| return { codeActions: undefined, sourceDisplay: undefined }; | ||||||||
| } | ||||||||
|
|
@@ -4156,7 +4184,7 @@ function getCompletionData( | |||||||
| */ | ||||||||
| function tryGetObjectLikeCompletionSymbols(): GlobalsSearch | undefined { | ||||||||
| const symbolsStartIndex = symbols.length; | ||||||||
| const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken); | ||||||||
| const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken, position); | ||||||||
| if (!objectLikeContainer) return GlobalsSearch.Continue; | ||||||||
|
|
||||||||
| // We're looking up possible property names from contextual/inferred/declared type. | ||||||||
|
|
@@ -4884,7 +4912,7 @@ function getCompletionData( | |||||||
| * Returns the immediate owning object literal or binding pattern of a context token, | ||||||||
| * on the condition that one exists and that the context implies completion should be given. | ||||||||
| */ | ||||||||
| function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined): ObjectLiteralExpression | ObjectBindingPattern | undefined { | ||||||||
| function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined, position: number): ObjectLiteralExpression | ObjectBindingPattern | undefined { | ||||||||
| if (contextToken) { | ||||||||
| const { parent } = contextToken; | ||||||||
| switch (contextToken.kind) { | ||||||||
|
|
@@ -4899,8 +4927,30 @@ function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined): Ob | |||||||
| case SyntaxKind.AsyncKeyword: | ||||||||
| return tryCast(parent.parent, isObjectLiteralExpression); | ||||||||
| case SyntaxKind.Identifier: | ||||||||
| return (contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent) | ||||||||
| ? contextToken.parent.parent : undefined; | ||||||||
| if ((contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent)) { | ||||||||
| return contextToken.parent.parent; | ||||||||
| } | ||||||||
| else { | ||||||||
| if (isObjectLiteralExpression(contextToken.parent.parent) && | ||||||||
| (isSpreadAssignment(contextToken.parent) || isShorthandPropertyAssignment(contextToken.parent) && | ||||||||
| (getLineAndCharacterOfPosition(contextToken.getSourceFile(), contextToken.getEnd()).line !== getLineAndCharacterOfPosition(contextToken.getSourceFile(), position).line))) { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass through the |
||||||||
| return contextToken.parent.parent; | ||||||||
| } | ||||||||
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as my comment above regarding spread assignments, I think you can handle them together with property assignment: |
||||||||
| if (ancestorNode && ancestorNode.getLastToken() === contextToken && isObjectLiteralExpression(ancestorNode.parent)) { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| return ancestorNode.parent; | ||||||||
| } | ||||||||
| } | ||||||||
| break; | ||||||||
| default: | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, I've been thinking about the difference between interface A {
a: string;
b: number;
foo(): void;
}
const b = 3;
const a: A = {
foo() {
},
b: b
/**/
}I have a feeling that we might want to be using
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, this is not a new test case, it's the same as this one I previously commented: const k: I = {
["e"]: i
/**/
}The relevant factor seems to be that there is an identifier in the right side of the property assignment.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, foo | // contextToken: foo, previousToken: foo
foo b| // contextToken: foo, previousToken: bThe idea is in most places, the completions should be the same whether or not you’ve already started typing something at the current location, so
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added test cases to check when the beginning of the expected completion is typed. |
||||||||
| if (parent.parent && parent.parent.parent && isMethodDeclaration(parent.parent) && isObjectLiteralExpression(parent.parent.parent)) { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| return parent.parent.parent; | ||||||||
| } | ||||||||
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| if (contextToken.kind !== SyntaxKind.ColonToken && ancestorNode && ancestorNode.getLastToken() === contextToken && | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| isObjectLiteralExpression(ancestorNode.parent)) { | ||||||||
| return ancestorNode.parent; | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
| //// interface ColorPalette { | ||
| //// primary?: string; | ||
| //// secondary?: string; | ||
| //// } | ||
|
|
||
| //// let colors: ColorPalette = { | ||
| //// primary: "red" | ||
| //// /**/ | ||
| //// }; | ||
|
|
||
| verify.completions({ | ||
| marker: "", | ||
| includes: [{ | ||
| name: "secondary", | ||
| sortText: completion.SortText.OptionalMember, | ||
| hasAction: true, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| }], | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); | ||
|
|
||
| verify.applyCodeActionFromCompletion("", { | ||
| name: "secondary", | ||
| description: `Add missing comma for object member completion 'secondary'.`, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| newFileContent: | ||
| `interface ColorPalette { | ||
| primary?: string; | ||
| secondary?: string; | ||
| } | ||
| let colors: ColorPalette = { | ||
| primary: "red", | ||
|
|
||
| };`, | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
| //// interface ColorPalette { | ||
| //// primary?: string; | ||
| //// secondary?: string; | ||
| //// } | ||
|
|
||
| //// interface I { | ||
| //// color: ColorPalette; | ||
| //// } | ||
|
|
||
| //// const a: I = { | ||
| //// color: {primary: "red" /**/} | ||
| //// } | ||
|
|
||
| verify.completions({ | ||
| marker: "", | ||
| includes: [{ | ||
| name: "secondary", | ||
| sortText: completion.SortText.OptionalMember, | ||
| hasAction: true, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| }], | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| } | ||
| }); | ||
|
|
||
| verify.applyCodeActionFromCompletion("", { | ||
| name: "secondary", | ||
| description: `Add missing comma for object member completion 'secondary'.`, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| newFileContent: | ||
| `interface ColorPalette { | ||
| primary?: string; | ||
| secondary?: string; | ||
| } | ||
| interface I { | ||
| color: ColorPalette; | ||
| } | ||
| const a: I = { | ||
| color: {primary: "red", } | ||
| }`, | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
| ////interface T { | ||
| //// aaa?: string; | ||
| //// foo(): void; | ||
| //// } | ||
| //// const obj: T = { | ||
| //// foo() { | ||
| // | ||
| //// } | ||
| //// /**/ | ||
| //// } | ||
|
|
||
| verify.completions({ | ||
| marker: "", | ||
| includes: [{ | ||
| name: "aaa", | ||
| sortText: completion.SortText.OptionalMember, | ||
| hasAction: true, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| }], | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); | ||
|
|
||
| verify.applyCodeActionFromCompletion("", { | ||
| name: "aaa", | ||
| description: `Add missing comma for object member completion 'aaa'.`, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| newFileContent: | ||
| `interface T { | ||
| aaa?: string; | ||
| foo(): void; | ||
| } | ||
| const obj: T = { | ||
| foo() { | ||
| }, | ||
|
|
||
| }`, | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
| ////interface T { | ||
| //// aaa: number; | ||
| //// bbb?: number; | ||
| //// } | ||
| //// const obj: T = { | ||
| //// aaa: 1 * (2 + 3) | ||
| //// /**/ | ||
| //// } | ||
|
|
||
| verify.completions({ | ||
| marker: "", | ||
| includes: [{ | ||
| name: "bbb", | ||
| sortText: completion.SortText.OptionalMember, | ||
| hasAction: true, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| }], | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); | ||
|
|
||
| verify.applyCodeActionFromCompletion("", { | ||
| name: "bbb", | ||
| description: `Add missing comma for object member completion 'bbb'.`, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| newFileContent: | ||
| `interface T { | ||
| aaa: number; | ||
| bbb?: number; | ||
| } | ||
| const obj: T = { | ||
| aaa: 1 * (2 + 3), | ||
|
|
||
| }`, | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /// <reference path="fourslash.ts" /> | ||
|
|
||
| //// type E = {} | ||
| //// type F = string | ||
| //// interface I { e: E, f?: F } | ||
| //// const i: I = { e: {} | ||
| //// /**/ | ||
| //// }; | ||
|
|
||
| verify.completions({ | ||
| marker: "", | ||
| includes: [{ | ||
| name: "f", | ||
| sortText: completion.SortText.OptionalMember, | ||
| hasAction: true, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| }], | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); | ||
|
|
||
| verify.applyCodeActionFromCompletion("", { | ||
| name: "f", | ||
| description: `Add missing comma for object member completion 'f'.`, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| newFileContent: | ||
| `type E = {} | ||
| type F = string | ||
| interface I { e: E, f?: F } | ||
| const i: I = { e: {}, | ||
|
|
||
| };`, | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
|
|
||
|
|
||
| /// <reference path="fourslash.ts" /> | ||
|
|
||
| //// type E = {} | ||
| //// type F = string | ||
| //// const i= { e: {} }; | ||
| //// interface I { e: E, f?: F } | ||
| //// const k: I = { | ||
| //// ["e"]: i | ||
| //// /**/ | ||
| //// } | ||
|
|
||
| verify.completions({ | ||
| marker: "", | ||
| includes: [{ | ||
| name: "f", | ||
| sortText: completion.SortText.OptionalMember, | ||
| hasAction: true, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| }], | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); | ||
|
|
||
| verify.applyCodeActionFromCompletion("", { | ||
| name: "f", | ||
| description: `Add missing comma for object member completion 'f'.`, | ||
| source: completion.CompletionSource.ObjectLiteralMemberWithComma, | ||
| newFileContent: | ||
| `type E = {} | ||
| type F = string | ||
| const i= { e: {} }; | ||
| interface I { e: E, f?: F } | ||
| const k: I = { | ||
| ["e"]: i, | ||
|
|
||
| }`, | ||
| preferences: { | ||
| allowIncompleteCompletions: true, | ||
| includeInsertTextCompletions: true, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave a comment above here with an example of what you're trying to capture.