Fix(52604): Provide Object member completions without comma; insert a comma#52899
Fix(52604): Provide Object member completions without comma; insert a comma#52899DanielRosenwasser merged 18 commits intomainfrom
Conversation
andrewbranch
left a comment
There was a problem hiding this comment.
Nice work, this is 90% of the way there. Just a few things to clean up.
src/services/completions.ts
Outdated
| /** Case completions for switch statements */ | ||
| SwitchCases = "SwitchCases/", | ||
| /** Completions for an Object literal expression */ | ||
| ObjectLiteralExpression = "ObjectLiteralExpression/", |
There was a problem hiding this comment.
The name of this should indicate that it’s for comma insertion. ObjectLiteralMemberWithComma or something
src/services/textChanges.ts
Outdated
| this.replaceNode(sourceFile, oldNode, newNode, { suffix }); | ||
| } | ||
|
|
||
| public replacePropertyAssignmentOnSameLine(sourceFile: SourceFile, oldNode: PropertyAssignment, newNode: PropertyAssignment): void { |
| }, | ||
| "Add missing comma for an object member completion '{0}'.": { | ||
| "category": "Message", | ||
| "code": 18052 |
There was a problem hiding this comment.
Although I don’t care as much about diagnostic message grouping as Jake does, it’s still nice to separate the Messages from the Errors. There should be a group that’s entirely Message category.
src/services/completions.ts
Outdated
| if (source === CompletionSource.ObjectLiteralExpression && contextToken) { | ||
| const changes = textChanges.ChangeTracker.with( | ||
| { host, formatContext, preferences }, | ||
| tracker=>tracker.insertText(sourceFile, contextToken.end,",")); |
There was a problem hiding this comment.
| tracker=>tracker.insertText(sourceFile, contextToken.end,",")); | |
| tracker => tracker.insertText(sourceFile, contextToken.end,",")); |
|
I wonder if this works on the playground 🤔 @typescript-bot pack this |
|
Heya @andrewbranch, I've started to run the tarball bundle task on this PR at c8c1362. You can monitor the build here. |
|
Hey @andrewbranch, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running |
src/services/completions.ts
Outdated
| if ((contextToken && isPropertyAssignment(contextToken.parent) && findNextToken(contextToken, contextToken?.parent, sourceFile)?.kind !== SyntaxKind.CommaToken && | ||
| completionKind === CompletionKind.ObjectPropertyDeclaration)) { |
There was a problem hiding this comment.
completionKind === CompletionKind.ObjectPropertyDeclaration is the cheapest of these conditions to check (along with contextToken), so it should be moved before the more expensive ones (particularly the findNextToken call) so the harder work can be short-circuited.
(Also, it looks like there’s an extra pair of parens around this whole expression.)
|
I saw that Navya already commented about this on the other issue, but before shipping this we should really fix microsoft/vscode#174628 or at least update the code to distinguish between sources. |
|
Another issue I found testing this: we don't do the comma insertion for object literal method snippet completions, only for the regular object literal completions: interface T {
aaa: string;
foo(): void;
}
const obj: T = {
aaa: ""
/**/
}If you select the completion that inserts just |
|
Something else I noticed that doesn't work is that we don't offer object literal completions after a commaless method, or in fact after a property assignment with a more complex expression: interface T {
aaa: string;
bbb: number;
foo(): void;
}
const obj: T = {
foo() {
}
/**/
}
const obj: T = {
bbb: 1 * 2
/**/
}You get the global completions here instead. |
|
Hm, good catch. The infrastructure we have now may not be conducive to combining the snippet and the comma insertion, since each has its own Inserting a comma after a method is probably a simple fix though. |
|
@typescript-bot pack this |
|
Heya @jakebailey, I've started to run the tarball bundle task on this PR at c8c1362. You can monitor the build here. |
|
Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running There is also a playground for this build and an npm module you can use via |
|
Heya @gabritto, I've started to run the diff-based user code test suite (tsserver) on this PR at 2fff618. You can monitor the build here. Update: The results are in! |
|
Heya @gabritto, I've started to run the perf test suite on this PR at 2fff618. You can monitor the build here. Update: The results are in! |
|
Hey @gabritto, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running There is also a playground for this build and an npm module you can use via |
src/services/completions.ts
Outdated
| if (source === CompletionSource.ObjectLiteralMemberWithComma && contextToken) { | ||
| const changes = textChanges.ChangeTracker.with( | ||
| { host, formatContext, preferences }, | ||
| tracker => tracker.insertText(sourceFile, contextToken.end,",")); |
There was a problem hiding this comment.
| tracker => tracker.insertText(sourceFile, contextToken.end,",")); | |
| tracker => tracker.insertText(sourceFile, contextToken.end, ","), | |
| ); |
src/services/completions.ts
Outdated
|
|
||
| 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 || |
There was a problem hiding this comment.
- You don't need to annotate the type of
node,it'll be inferred (edit: but you can also just writeisPropertyAssignmentdirectly. - Feed through the
sourceFilewhen usingNodemethods so that they don't have to walk back up the tree.
| (isMethodDeclaration(contextToken.parent.parent) || isSpreadAssignment(contextToken.parent) || findAncestor(contextToken.parent, (node: Node) => isPropertyAssignment(node))?.getLastToken() === contextToken || | |
| (isMethodDeclaration(contextToken.parent.parent) || isSpreadAssignment(contextToken.parent) || findAncestor(contextToken.parent, node => isPropertyAssignment(node))?.getLastToken(sourceFile) === contextToken || |
There was a problem hiding this comment.
I'd also rather you just broke this into 2 nested ifs, where it looks like
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;
}
}There was a problem hiding this comment.
Wait, you don't need node => isPropertyAssignment, just use isPropertyAssignment directly.
|
@gabritto Here are the results of running the user test suite comparing Everything looks good! |
src/services/completions.ts
Outdated
| hasAction = true; | ||
| } | ||
|
|
||
| if (completionKind === CompletionKind.ObjectPropertyDeclaration && contextToken && |
There was a problem hiding this comment.
Leave a comment above here with an example of what you're trying to capture.
src/services/completions.ts
Outdated
| else { | ||
| if (isObjectLiteralExpression(contextToken.parent.parent) && | ||
| (isSpreadAssignment(contextToken.parent) || isShorthandPropertyAssignment(contextToken.parent) && | ||
| (getLineAndCharacterOfPosition(contextToken.getSourceFile(), contextToken.getEnd()).line !== getLineAndCharacterOfPosition(contextToken.getSourceFile(), position).line))) { |
There was a problem hiding this comment.
Pass through the sourceFile here too and reuse the same sourceFile
src/services/completions.ts
Outdated
| (getLineAndCharacterOfPosition(contextToken.getSourceFile(), contextToken.getEnd()).line !== getLineAndCharacterOfPosition(contextToken.getSourceFile(), position).line))) { | ||
| return contextToken.parent.parent; | ||
| } | ||
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); |
There was a problem hiding this comment.
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); | |
| const ancestorNode = findAncestor(parent, isPropertyAssignment); | |
src/services/completions.ts
Outdated
| if (parent.parent && parent.parent.parent && isMethodDeclaration(parent.parent) && isObjectLiteralExpression(parent.parent.parent)) { | ||
| return parent.parent.parent; | ||
| } | ||
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); |
There was a problem hiding this comment.
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); | |
| const ancestorNode = findAncestor(parent, isPropertyAssignment); |
src/services/completions.ts
Outdated
| return parent.parent.parent; | ||
| } | ||
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); | ||
| if (contextToken.kind !== SyntaxKind.ColonToken && ancestorNode && ancestorNode.getLastToken() === contextToken && |
There was a problem hiding this comment.
| if (contextToken.kind !== SyntaxKind.ColonToken && ancestorNode && ancestorNode.getLastToken() === contextToken && | |
| if (contextToken.kind !== SyntaxKind.ColonToken && ancestorNode?.getLastToken() === contextToken && |
src/services/completions.ts
Outdated
| } | ||
| break; | ||
| default: | ||
| if (parent.parent && parent.parent.parent && isMethodDeclaration(parent.parent) && isObjectLiteralExpression(parent.parent.parent)) { |
There was a problem hiding this comment.
| if (parent.parent && parent.parent.parent && isMethodDeclaration(parent.parent) && isObjectLiteralExpression(parent.parent.parent)) { | |
| if (parent.parent?.parent && isMethodDeclaration(parent.parent) && isObjectLiteralExpression(parent.parent.parent)) { |
src/services/completions.ts
Outdated
| return contextToken.parent.parent; | ||
| } | ||
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); | ||
| if (ancestorNode && ancestorNode.getLastToken() === contextToken && isObjectLiteralExpression(ancestorNode.parent)) { |
There was a problem hiding this comment.
| if (ancestorNode && ancestorNode.getLastToken() === contextToken && isObjectLiteralExpression(ancestorNode.parent)) { | |
| if (ancestorNode?.getLastToken() === contextToken && isObjectLiteralExpression(ancestorNode.parent)) { | |
src/services/completions.ts
Outdated
|
|
||
| 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 || |
There was a problem hiding this comment.
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:
findAncestor(contextToken.parent, node => isPropertyAssignment(node) || isSpreadAssignment(node))?.getLastToken()
src/services/completions.ts
Outdated
| (getLineAndCharacterOfPosition(contextToken.getSourceFile(), contextToken.getEnd()).line !== getLineAndCharacterOfPosition(contextToken.getSourceFile(), position).line))) { | ||
| return contextToken.parent.parent; | ||
| } | ||
| const ancestorNode = findAncestor(parent, (node: Node) => isPropertyAssignment(node)); |
There was a problem hiding this comment.
Same as my comment above regarding spread assignments, I think you can handle them together with property assignment:
const ancestorNode = findAncestor(parent, node => isPropertyAssignment(node) || isSpreadAssignment(node));
|
@gabritto Here they are:
CompilerComparison Report - main..52899
System
Hosts
Scenarios
TSServerComparison Report - main..52899
System
Hosts
Scenarios
StartupComparison Report - main..52899
System
Hosts
Scenarios
Developer Information: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
You should really be using export type ObjectLiteralElementLike
= PropertyAssignment
| ShorthandPropertyAssignment
| SpreadAssignment
| MethodDeclaration
| AccessorDeclaration
;But I've noticed you don't provide the comma for accessors. interface SomeType {
first: number;
second: number
}
export let x: SomeType = {
get first() { return 42 }
/**/
} |
Yeah, I've added a test for that now. |
|
@gabritto Here are the results of running the top-repos suite comparing Something interesting changed - please have a look. Details
|
This pr provides object member completion with a missing comma and inserts a comma as well. It inserts comma in cases when insertion is requested on the same line,
color: {primary: "red" /*$*/}or on different lines,and excludes cases which do not need a comma, like
const i: I = {/*$*/};orFixes #52604