From 33f67df6ff5bd1cd75fd0f1621a61a8bcc4eaf5d Mon Sep 17 00:00:00 2001 From: Yuriy Artamonov Date: Tue, 21 Feb 2023 15:29:55 +0100 Subject: [PATCH] [kotlin] KTIJ-24655 New File when called outside of Kotlin source roots suggests only Kotlin Script/Worksheet files GitOrigin-RevId: 11de37bf74f63f613748ac89b1d80f53d9cae43c --- .../messages/KotlinBundle.properties | 10 +- .../idea/actions/NewKotlinFileAction.kt | 229 +++++++++--------- .../idea/actions/NewKotlinScriptAction.kt | 51 ++++ .../extractClass/ExtractSuperRefactoring.kt | 1 - .../idea/actions/NewKotlinFileActionTest.kt | 5 +- .../common/resources/META-INF/kotlin-core.xml | 8 +- 6 files changed, 186 insertions(+), 118 deletions(-) create mode 100644 plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinScriptAction.kt diff --git a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties index 2464e1ebf8ee..d3329fa936e7 100644 --- a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties +++ b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties @@ -347,9 +347,15 @@ action.generate.tostring.choose.implementation.mnemonic=i action.generate.tostring.generate.super.call=Generate call to super.toString() action.generate.tostring.generate.super.call.mnemonic=s -action.new.file.text=Kotlin Class/File -action.new.file.description=Creates new Kotlin class or file +action.Kotlin.NewFile.text=Kotlin Class/File +action.Kotlin.NewFile.description=Creates new Kotlin class or file + +action.Kotlin.NewScript.text=Kotlin Script +action.Kotlin.NewScript.description=Creates new Kotlin script or worksheet + action.new.file.dialog.title=New Kotlin Class/File +action.new.script.dialog.title=New Kotlin File + action.new.file.dialog.file.title=File action.new.file.dialog.class.title=Class action.new.file.dialog.data.class.title=Data class diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinFileAction.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinFileAction.kt index 93de9c843fdc..095d75301447 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinFileAction.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinFileAction.kt @@ -7,6 +7,9 @@ import com.intellij.ide.fileTemplates.FileTemplate import com.intellij.ide.fileTemplates.FileTemplateManager import com.intellij.ide.fileTemplates.actions.AttributesDefaults import com.intellij.ide.fileTemplates.ui.CreateFromTemplateDialog +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.editor.Editor @@ -23,7 +26,7 @@ import com.intellij.openapi.util.registry.RegistryManager import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiFile import com.intellij.util.IncorrectOperationException -import org.jetbrains.annotations.TestOnly +import org.jetbrains.annotations.ApiStatus import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.idea.KotlinFileType @@ -43,11 +46,112 @@ import org.jetbrains.kotlin.psi.KtNamedDeclaration import org.jetbrains.kotlin.psi.psiUtil.startOffset import java.util.* -internal class NewKotlinFileAction : CreateFileFromTemplateAction( - KotlinBundle.message("action.new.file.text"), - KotlinBundle.message("action.new.file.description"), - KotlinFileType.INSTANCE.icon -), DumbAware { +internal class NewKotlinFileAction : AbstractNewKotlinFileAction(), DumbAware { + override fun isAvailable(dataContext: DataContext): Boolean { + if (!super.isAvailable(dataContext)) return false + + val ideView = LangDataKeys.IDE_VIEW.getData(dataContext) ?: return false + val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return false + val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex + + return ideView.directories.any { + projectFileIndex.isInSourceContent(it.virtualFile) || + CreateTemplateInPackageAction.isInContentRoot(it.virtualFile, projectFileIndex) + } + } + + override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) { + val sealedTemplatesEnabled = RegistryManager.getInstance().`is`("kotlin.create.sealed.templates.enabled") + + builder.setTitle(KotlinBundle.message("action.new.file.dialog.title")) + + builder + .addKind( + KotlinBundle.message("action.new.file.dialog.class.title"), + KotlinIcons.CLASS, + "Kotlin Class" + ) + .addKind( + KotlinBundle.message("action.new.file.dialog.file.title"), + KotlinFileType.INSTANCE.icon, + "Kotlin File" + ) + .addKind( + KotlinBundle.message("action.new.file.dialog.interface.title"), + KotlinIcons.INTERFACE, + "Kotlin Interface" + ) + + if (sealedTemplatesEnabled && project.languageVersionSettings.supportsFeature(LanguageFeature.SealedInterfaces)) { + builder.addKind( + KotlinBundle.message("action.new.file.dialog.sealed.interface.title"), + KotlinIcons.INTERFACE, + "Kotlin Sealed Interface" + ) + } + + builder + .addKind( + KotlinBundle.message("action.new.file.dialog.data.class.title"), + KotlinIcons.CLASS, + "Kotlin Data Class" + ) + .addKind( + KotlinBundle.message("action.new.file.dialog.enum.title"), + KotlinIcons.ENUM, + "Kotlin Enum" + ) + + if (sealedTemplatesEnabled) { + builder.addKind( + KotlinBundle.message("action.new.file.dialog.sealed.class.title"), + KotlinIcons.CLASS, + "Kotlin Sealed Class" + ) + } + + builder + .addKind( + KotlinBundle.message("action.new.file.dialog.annotation.title"), + KotlinIcons.ANNOTATION, + "Kotlin Annotation" + ) + + builder + .addKind( + KotlinBundle.message("action.new.script.name"), + KotlinIcons.SCRIPT, + KOTLIN_SCRIPT_TEMPLATE_NAME + ) + .addKind( + KotlinBundle.message("action.new.worksheet.name"), + KotlinIcons.SCRIPT, + KOTLIN_WORKSHEET_TEMPLATE_NAME + ) + + builder + .addKind( + KotlinBundle.message("action.new.file.dialog.object.title"), + KotlinIcons.OBJECT, + "Kotlin Object" + ) + + builder.setValidator(NewKotlinFileNameValidator) + } + + override fun getActionName(directory: PsiDirectory, newName: String, templateName: String): String = + KotlinBundle.message("action.Kotlin.NewFile.text") + + override fun hashCode(): Int = 0 + + override fun equals(other: Any?): Boolean = other is NewKotlinFileAction +} + +internal abstract class AbstractNewKotlinFileAction : CreateFileFromTemplateAction() { + + private fun KtFile.editor(): Editor? = + FileEditorManager.getInstance(this.project).selectedTextEditor?.takeIf { it.document == this.viewProvider.document } + override fun postProcess(createdElement: PsiFile, templateName: String?, customProperties: Map?) { super.postProcess(createdElement, templateName, customProperties) @@ -80,107 +184,10 @@ internal class NewKotlinFileAction : CreateFileFromTemplateAction( } } - private fun KtFile.editor(): Editor? = - FileEditorManager.getInstance(this.project).selectedTextEditor?.takeIf { it.document == this.viewProvider.document } - - override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) { - val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex - val isInSourceRoot = projectFileIndex.isInSourceContent(directory.virtualFile) || - CreateTemplateInPackageAction.isInContentRoot(directory.virtualFile, projectFileIndex) - - val sealedTemplatesEnabled = RegistryManager.getInstance().`is`("kotlin.create.sealed.templates.enabled") - - builder.setTitle(KotlinBundle.message("action.new.file.dialog.title")) - - if (isInSourceRoot) { - builder - .addKind( - KotlinBundle.message("action.new.file.dialog.class.title"), - KotlinIcons.CLASS, - "Kotlin Class" - ) - .addKind( - KotlinBundle.message("action.new.file.dialog.file.title"), - KotlinFileType.INSTANCE.icon, - "Kotlin File" - ) - .addKind( - KotlinBundle.message("action.new.file.dialog.interface.title"), - KotlinIcons.INTERFACE, - "Kotlin Interface" - ) - - if (sealedTemplatesEnabled && project.languageVersionSettings.supportsFeature(LanguageFeature.SealedInterfaces)) { - builder.addKind( - KotlinBundle.message("action.new.file.dialog.sealed.interface.title"), - KotlinIcons.INTERFACE, - "Kotlin Sealed Interface" - ) - } - - builder - .addKind( - KotlinBundle.message("action.new.file.dialog.data.class.title"), - KotlinIcons.CLASS, - "Kotlin Data Class" - ) - .addKind( - KotlinBundle.message("action.new.file.dialog.enum.title"), - KotlinIcons.ENUM, - "Kotlin Enum" - ) - - if (sealedTemplatesEnabled) { - builder.addKind( - KotlinBundle.message("action.new.file.dialog.sealed.class.title"), - KotlinIcons.CLASS, - "Kotlin Sealed Class" - ) - } - - builder - .addKind( - KotlinBundle.message("action.new.file.dialog.annotation.title"), - KotlinIcons.ANNOTATION, - "Kotlin Annotation" - ) - } - - builder - .addKind( - KotlinBundle.message("action.new.script.name"), - KotlinIcons.SCRIPT, - "Kotlin Script" - ) - .addKind( - KotlinBundle.message("action.new.worksheet.name"), - KotlinIcons.SCRIPT, - "Kotlin Worksheet" - ) - - if (isInSourceRoot) { - builder - .addKind( - KotlinBundle.message("action.new.file.dialog.object.title"), - KotlinIcons.OBJECT, - "Kotlin Object" - ) - } - - builder.setValidator(NameValidator) - } - - override fun getActionName(directory: PsiDirectory, newName: String, templateName: String): String = - KotlinBundle.message("action.new.file.text") - - override fun hashCode(): Int = 0 - - override fun equals(other: Any?): Boolean = other is NewKotlinFileAction - override fun startInWriteAction() = false override fun createFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? { - val targetTemplate = if ("Kotlin Worksheet" != template.name) { + val targetTemplate = if (KOTLIN_WORKSHEET_TEMPLATE_NAME != template.name) { template } else { object : FileTemplate by template { @@ -192,7 +199,8 @@ internal class NewKotlinFileAction : CreateFileFromTemplateAction( } } -private object NameValidator : InputValidatorEx { +@ApiStatus.Internal +object NewKotlinFileNameValidator : InputValidatorEx { override fun getErrorText(inputString: String): String? { if (inputString.trim().isEmpty()) { return KotlinBundle.message("action.new.file.error.empty.name") @@ -211,10 +219,6 @@ private object NameValidator : InputValidatorEx { override fun canClose(inputString: String): Boolean = getErrorText(inputString) == null } -@get:TestOnly -val createFileNameValidator: InputValidatorEx - get() = NameValidator - private fun findOrCreateTarget(dir: PsiDirectory, name: String, directorySeparators: CharArray): Pair { var className = removeKotlinExtensionIfPresent(name) var targetDir = dir @@ -238,6 +242,9 @@ private fun findOrCreateTarget(dir: PsiDirectory, name: String, directorySeparat const val KOTLIN_WORKSHEET_EXTENSION: String = "ws.kts" +internal const val KOTLIN_WORKSHEET_TEMPLATE_NAME: String = "Kotlin Worksheet" +internal const val KOTLIN_SCRIPT_TEMPLATE_NAME: String = "Kotlin Script" + private fun removeKotlinExtensionIfPresent(name: String): String = when { name.endsWith(".$KOTLIN_WORKSHEET_EXTENSION") -> name.removeSuffix(".$KOTLIN_WORKSHEET_EXTENSION") name.endsWith(".$STD_SCRIPT_SUFFIX") -> name.removeSuffix(".$STD_SCRIPT_SUFFIX") @@ -270,12 +277,12 @@ private fun createKotlinFileFromTemplate(dir: PsiDirectory, className: String, t private val FILE_SEPARATORS: CharArray = charArrayOf('/', '\\') private val FQNAME_SEPARATORS: CharArray = charArrayOf('/', '\\', '.') -private fun createFileFromTemplateWithStat(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? { +internal fun createFileFromTemplateWithStat(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? { KotlinCreateFileFUSCollector.logFileTemplate(template.name) return createKotlinFileFromTemplate(name, template, dir) } -fun createKotlinFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? { +internal fun createKotlinFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory): PsiFile? { val directorySeparators = when (template.name) { "Kotlin File" -> FILE_SEPARATORS "Kotlin Worksheet" -> FILE_SEPARATORS diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinScriptAction.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinScriptAction.kt new file mode 100644 index 000000000000..27c36f8d0644 --- /dev/null +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/actions/NewKotlinScriptAction.kt @@ -0,0 +1,51 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + +package org.jetbrains.kotlin.idea.actions + +import com.intellij.ide.actions.CreateFileFromTemplateDialog +import com.intellij.ide.actions.CreateTemplateInPackageAction +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.LangDataKeys +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.psi.PsiDirectory +import org.jetbrains.kotlin.idea.KotlinIcons +import org.jetbrains.kotlin.idea.base.resources.KotlinBundle + +internal class NewKotlinScriptAction : AbstractNewKotlinFileAction(), DumbAware { + override fun isAvailable(dataContext: DataContext): Boolean { + if (!super.isAvailable(dataContext)) return false + + val ideView = LangDataKeys.IDE_VIEW.getData(dataContext) ?: return false + val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return false + val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex + + return ideView.directories.none { + projectFileIndex.isInSourceContent(it.virtualFile) || + CreateTemplateInPackageAction.isInContentRoot(it.virtualFile, projectFileIndex) + } + } + + override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) { + builder.setTitle(KotlinBundle.message("action.new.script.dialog.title")) + + builder + .addKind( + KotlinBundle.message("action.new.script.name"), + KotlinIcons.SCRIPT, + KOTLIN_SCRIPT_TEMPLATE_NAME + ) + .addKind( + KotlinBundle.message("action.new.worksheet.name"), + KotlinIcons.SCRIPT, + KOTLIN_WORKSHEET_TEMPLATE_NAME + ) + + builder.setValidator(NewKotlinFileNameValidator) + } + + override fun getActionName(directory: PsiDirectory, newName: String, templateName: String): String = + KotlinBundle.message("action.Kotlin.NewScript.text") +} \ No newline at end of file diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ExtractSuperRefactoring.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ExtractSuperRefactoring.kt index ec2faad674c9..39041bf9d090 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ExtractSuperRefactoring.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/extractClass/ExtractSuperRefactoring.kt @@ -20,7 +20,6 @@ import org.jetbrains.kotlin.asJava.toLightClass import org.jetbrains.kotlin.asJava.toLightMethods import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.idea.base.resources.KotlinBundle -import org.jetbrains.kotlin.idea.actions.NewKotlinFileAction import org.jetbrains.kotlin.idea.actions.createKotlinFileFromTemplate import org.jetbrains.kotlin.idea.base.psi.copied import org.jetbrains.kotlin.idea.base.psi.replaced diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/actions/NewKotlinFileActionTest.kt b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/actions/NewKotlinFileActionTest.kt index 6824b601c6dd..465e3f4d9fc1 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/actions/NewKotlinFileActionTest.kt +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/actions/NewKotlinFileActionTest.kt @@ -3,7 +3,6 @@ package org.jetbrains.kotlin.idea.actions import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase -import org.junit.Assert import org.junit.internal.runners.JUnit38ClassRunner import org.junit.runner.RunWith @@ -51,7 +50,7 @@ class NewKotlinFileActionTest : LightJavaCodeInsightFixtureTestCase() { } private fun validateName(name: String, errorMessage: String?) { - val actualError = createFileNameValidator.getErrorText(name) - Assert.assertEquals("Invalid error message", errorMessage, actualError) + val actualError = NewKotlinFileNameValidator.getErrorText(name) + assertEquals("Invalid error message", errorMessage, actualError) } } \ No newline at end of file diff --git a/plugins/kotlin/plugin/common/resources/META-INF/kotlin-core.xml b/plugins/kotlin/plugin/common/resources/META-INF/kotlin-core.xml index 235bdafad13e..267b019c6628 100644 --- a/plugins/kotlin/plugin/common/resources/META-INF/kotlin-core.xml +++ b/plugins/kotlin/plugin/common/resources/META-INF/kotlin-core.xml @@ -475,10 +475,16 @@ - + + + + +