-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Fix(52604): Provide Object member completions without comma; insert a comma #52899
Conversation
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.
Nice work, this is 90% of the way there. Just a few things to clean up.
src/services/completions.ts
Outdated
@@ -423,6 +425,8 @@ export enum CompletionSource { | |||
ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/", | |||
/** Case completions for switch statements */ | |||
SwitchCases = "SwitchCases/", | |||
/** Completions for an Object literal expression */ | |||
ObjectLiteralExpression = "ObjectLiteralExpression/", |
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.
The name of this should indicate that it’s for comma insertion. ObjectLiteralMemberWithComma
or something
src/services/textChanges.ts
Outdated
@@ -592,6 +592,11 @@ export class ChangeTracker { | |||
this.replaceNode(sourceFile, oldNode, newNode, { suffix }); | |||
} | |||
|
|||
public replacePropertyAssignmentOnSameLine(sourceFile: SourceFile, oldNode: PropertyAssignment, newNode: PropertyAssignment): void { |
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.
This is no longer used.
src/compiler/diagnosticMessages.json
Outdated
}, | ||
"Add missing comma for an object member completion '{0}'.": { | ||
"category": "Message", | ||
"code": 18052 |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tracker => tracker.insertText(sourceFile, contextToken.end,",")); | |
tracker => tracker.insertText(sourceFile, contextToken.end, ","), | |
); |
src/services/completions.ts
Outdated
@@ -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 || |
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.
- You don't need to annotate the type of
node,
it'll be inferred (edit: but you can also just writeisPropertyAssignment
directly. - Feed through the
sourceFile
when usingNode
methods 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also rather you just broke this into 2 nested if
s, 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -1683,6 +1686,15 @@ function createCompletionEntry( | |||
hasAction = true; | |||
} | |||
|
|||
if (completionKind === CompletionKind.ObjectPropertyDeclaration && contextToken && |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (ancestorNode && ancestorNode.getLastToken() === contextToken && isObjectLiteralExpression(ancestorNode.parent)) { | |
if (ancestorNode?.getLastToken() === contextToken && isObjectLiteralExpression(ancestorNode.parent)) { | |
src/services/completions.ts
Outdated
@@ -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 || |
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.
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.
Choose a reason for hiding this comment
The 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:
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