From a119bc85cc3db40d830b99c41d1ad4a0cc3c4936 Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Thu, 31 Jul 2025 18:28:59 +0200 Subject: [PATCH] [command-completion] IDEA-373902 Command completion. New actions: Generate constructors GitOrigin-RevId: 02d2af96982d53a0734c9c1f57a452758a184b85 --- .../resources/META-INF/JavaPlugin.xml | 1 + ...avaGenerateConstructorCompletionCommand.kt | 126 ++++++++++++++++++ ...mandsCompletionGenerateConstructorsTest.kt | 114 ++++++++++++++++ .../resources/messages/JavaBundle.properties | 2 + 4 files changed, 243 insertions(+) create mode 100644 java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/JavaGenerateConstructorCompletionCommand.kt create mode 100644 java/java-tests/testSrc/com/intellij/java/codeInsight/completion/commands/JavaCommandsCompletionGenerateConstructorsTest.kt diff --git a/java/java-impl/resources/META-INF/JavaPlugin.xml b/java/java-impl/resources/META-INF/JavaPlugin.xml index 6e96860c16a8..81912631cacf 100644 --- a/java/java-impl/resources/META-INF/JavaPlugin.xml +++ b/java/java-impl/resources/META-INF/JavaPlugin.xml @@ -1497,6 +1497,7 @@ + diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/JavaGenerateConstructorCompletionCommand.kt b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/JavaGenerateConstructorCompletionCommand.kt new file mode 100644 index 000000000000..85ea2af76513 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/JavaGenerateConstructorCompletionCommand.kt @@ -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() ?: 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() + } + + override fun getCommands(context: CommandCompletionProviderContext): List { + val clazz = findContext(context) ?: return emptyList() + val result = mutableListOf() + 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? { + return super.getAllOriginalMembers(aClass) + } + + public override fun generateMemberPrototypes(aClass: PsiClass?, members: Array?): List { + 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, + ): 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 + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/completion/commands/JavaCommandsCompletionGenerateConstructorsTest.kt b/java/java-tests/testSrc/com/intellij/java/codeInsight/completion/commands/JavaCommandsCompletionGenerateConstructorsTest.kt new file mode 100644 index 000000000000..b6a4621aac03 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/completion/commands/JavaCommandsCompletionGenerateConstructorsTest.kt @@ -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 + } + """.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 + } + """.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{ + int a; + g + } + + class B { + 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{ + int a; + + public A(String a, int a1) { + super(a); + this.a = a1; + } + } + + class B { + public B(T a){} + }""".trimIndent()) + } + + fun testNoGenerateNoArgsConstructor() { + myFixture.configureByText(JavaFileType.INSTANCE, """ + class A { + public A(){} + int a; + g + } + """.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 + } + + 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) }) + } +} \ No newline at end of file diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 849e5d4875b6..5345c8b46301 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -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