[command-completion] IDEA-373902 Command completion. New actions: Generate constructors

GitOrigin-RevId: 02d2af96982d53a0734c9c1f57a452758a184b85
This commit is contained in:
Mikhail Pyltsin
2025-07-31 18:17:37 +00:00
committed by intellij-monorepo-bot
parent 0fc0b74dd2
commit a119bc85cc
4 changed files with 243 additions and 0 deletions
@@ -1497,6 +1497,7 @@
<codeInsight.completion.command.provider language="JAVA" implementationClass="com.intellij.codeInsight.completion.commands.impl.JavaTypeHierarchyCompletionCommandProvider" order="last"/>
<codeInsight.completion.command.provider language="JAVA" implementationClass="com.intellij.codeInsight.completion.commands.impl.JavaTypeInfoCompletionCommandProvider" order="last"/>
<codeInsight.completion.command.provider language="JAVA" implementationClass="com.intellij.codeInsight.completion.commands.impl.JavaQuickDocumentationCompletionCommand" order="last"/>
<codeInsight.completion.command.provider language="JAVA" implementationClass="com.intellij.codeInsight.completion.commands.impl.JavaGenerateConstructorCompletionCommandProvider" order="last"/>
<weigher implementationClass="com.intellij.codeInsight.completion.LoggerWeigher" key="completion" id="logger"/>
@@ -0,0 +1,126 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.completion.commands.impl
import com.intellij.codeInsight.CodeInsightBundle
import com.intellij.codeInsight.completion.command.CommandCompletionProviderContext
import com.intellij.codeInsight.completion.command.CommandProvider
import com.intellij.codeInsight.completion.command.CompletionCommand
import com.intellij.codeInsight.completion.command.getCommandContext
import com.intellij.codeInsight.daemon.impl.quickfix.AddDefaultConstructorFix
import com.intellij.codeInsight.daemon.impl.quickfix.AddMethodFix
import com.intellij.codeInsight.generation.ClassMember
import com.intellij.codeInsight.generation.GenerateConstructorHandler
import com.intellij.codeInsight.generation.GenerationInfo
import com.intellij.codeInsight.generation.PsiMethodMember
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.java.JavaBundle
import com.intellij.modcommand.ActionContext
import com.intellij.modcommand.ModCommandExecutor
import com.intellij.openapi.editor.Editor
import com.intellij.psi.*
import com.intellij.psi.util.TypeConversionUtil
import com.intellij.psi.util.parentOfType
import org.jetbrains.annotations.Nls
public class JavaGenerateConstructorCompletionCommandProvider : CommandProvider {
private fun findContext(context: CommandCompletionProviderContext): PsiClass? {
val element = getCommandContext(context.offset, context.psiFile) ?: return null
val containingClass = element.parentOfType<PsiClass>() ?: return null
if (containingClass.isInterface || containingClass.isRecord || containingClass is PsiImplicitClass) return null
if (element is PsiIdentifier && element.parent is PsiClass) return element.parent as PsiClass
if (!(element is PsiWhiteSpace && element.text.contains("\n"))) return null
return element.parentOfType<PsiClass>()
}
override fun getCommands(context: CommandCompletionProviderContext): List<CompletionCommand> {
val clazz = findContext(context) ?: return emptyList()
val result = mutableListOf<CompletionCommand>()
if (clazz.constructors.none { it.parameters.isEmpty() }) {
result.add(object : CompletionCommand() {
private val fix = AddDefaultConstructorFix(clazz, PsiModifier.PUBLIC)
override val presentableName: @Nls String
get() = CodeInsightBundle.message("command.completion.generate.text", JavaBundle.message("command.completion.generate.no.args.constructor.text"))
override fun execute(offset: Int, psiFile: PsiFile, editor: Editor?) {
val actionContext = ActionContext.from(editor, psiFile)
ModCommandExecutor.executeInteractively(actionContext, presentableName, editor) {
fix.perform(actionContext)
}
}
override fun getPreview(): IntentionPreviewInfo {
val actionContext = ActionContext.from(context.editor, context.psiFile)
return fix.generatePreview(actionContext)
}
})
}
val handler = object : GenerateConstructorHandler() {
public override fun getAllOriginalMembers(aClass: PsiClass?): Array<out ClassMember?>? {
return super.getAllOriginalMembers(aClass)
}
public override fun generateMemberPrototypes(aClass: PsiClass?, members: Array<out ClassMember?>?): List<GenerationInfo?> {
return super.generateMemberPrototypes(aClass, members)
}
}
var members = handler.getAllOriginalMembers(clazz) ?: return result
if (members.isEmpty()) return result
//let's simplify condition, because it can be quite tricky to find out whether there is a constructor with the same arguments or
//which super constructor should be called
val superClass = clazz.superClass
if ((clazz.constructors.none { it.parameters.size == members.size } &&
(superClass == null ||
superClass.constructors.isEmpty() ||
superClass.constructors.any { it.parameters.isEmpty() })) ||
canUseSuperConstructor(superClass, clazz, members)) {
val superConstructors = superClass?.constructors
if (superConstructors?.size == 1) {
val substitutor = TypeConversionUtil.getSuperClassSubstitutor(superClass, clazz, PsiSubstitutor.EMPTY)
val superConstructor = PsiMethodMember(superConstructors[0], substitutor)
members = arrayOf(superConstructor, *members)
}
val prototypes = handler.generateMemberPrototypes(clazz, members)
if (prototypes.size == 1) {
val generationInfo = prototypes[0]
val text = (generationInfo?.psiMember as? PsiMethod)?.text ?: return result
result.add(object : CompletionCommand() {
private val fix = AddMethodFix(text, clazz)
override val presentableName: @Nls String
get() = CodeInsightBundle.message("command.completion.generate.text", JavaBundle.message("command.completion.generate.all.args.constructor.text"))
override fun execute(offset: Int, psiFile: PsiFile, editor: Editor?) {
val actionContext = ActionContext.from(editor, psiFile)
ModCommandExecutor.executeInteractively(actionContext, presentableName, editor) {
fix.perform(actionContext)
}
}
override fun getPreview(): IntentionPreviewInfo {
val actionContext = ActionContext.from(context.editor, context.psiFile)
return fix.generatePreview(actionContext)
}
})
}
}
return result
}
private fun canUseSuperConstructor(
superClass: PsiClass?,
clazz: PsiClass,
members: Array<out ClassMember?>,
): Boolean {
if (superClass == null) return false
if (superClass.constructors.size != 1) return false
val method = superClass.constructors[0]
val targetSize = members.size + method.parameters.size
return clazz.constructors.none {
it.parameters.size == targetSize
}
}
}
@@ -0,0 +1,114 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInsight.completion.commands
import com.intellij.codeInsight.completion.LightFixtureCompletionTestCase
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.application.impl.NonBlockingReadActionImpl
import com.intellij.openapi.util.registry.Registry
import com.intellij.testFramework.NeedsIndex
@NeedsIndex.SmartMode(reason = "it requires highlighting")
class JavaCommandsCompletionGenerateConstructorsTest : LightFixtureCompletionTestCase() {
override fun setUp() {
super.setUp()
Registry.get("ide.completion.command.enabled").setValue(false, getTestRootDisposable())
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())
}
fun testGenerateNoArgsConstructor() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
g<caret>
}
""".trimIndent())
val elements = myFixture.completeBasic()
selectItem(elements.first { element -> element.lookupString.contains("Generate 'No-Args Constructor'", ignoreCase = true) })
NonBlockingReadActionImpl.waitForAsyncTaskCompletion()
myFixture.checkResult("""
class A {
int a;
public A() {
}
}""".trimIndent())
}
fun testGenerateAllArgsConstructor() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
g<caret>
}
""".trimIndent())
val elements = myFixture.completeBasic()
selectItem(elements.first { element -> element.lookupString.contains("Generate 'All-Args Constructor'", ignoreCase = true) })
NonBlockingReadActionImpl.waitForAsyncTaskCompletion()
myFixture.checkResult("""
class A {
int a;
public A(int a) {
this.a = a;
}
}""".trimIndent())
}
fun testGenerateAllArgsConstructorWithSuper() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A extends B<String>{
int a;
g<caret>
}
class B<T> {
public B(T a){}
}
""".trimIndent())
val elements = myFixture.completeBasic()
selectItem(elements.first { element -> element.lookupString.contains("Generate 'All-Args Constructor'", ignoreCase = true) })
NonBlockingReadActionImpl.waitForAsyncTaskCompletion()
myFixture.checkResult("""
class A extends B<String>{
int a;
public A(String a, int a1) {
super(a);
this.a = a1;
}
}
class B<T> {
public B(T a){}
}""".trimIndent())
}
fun testNoGenerateNoArgsConstructor() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
public A(){}
int a;
g<caret>
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNull(elements.firstOrNull { element -> element.lookupString.contains("Generate 'No-Args Constructor'", ignoreCase = true) })
}
fun testNoGenerateAllArgsConstructor() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A extends B{
int a;
g<caret>
}
class B {
public B(int a){}
public B(String a){}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNull(elements.firstOrNull { element -> element.lookupString.contains("Generate 'All-Args Constructor'", ignoreCase = true) })
}
}
@@ -1954,6 +1954,8 @@ command.completion.getters.and.setters.text=Getters and Setters
command.completion.generate.getter.setter=Generate 'Getter' and 'Setter'
command.completion.generate.getter=Generate 'Getter'
command.completion.generate.setter=Generate 'Setter'
command.completion.generate.no.args.constructor.text=No-Args Constructor
command.completion.generate.all.args.constructor.text=All-Args Constructor
advanced.setting.java.show.irrelevant.templates.in.source.roots=Show irrelevant New File templates in Java source roots
java.test.use.wall.time=Use Wall Time