Move: Add intention to change file package without moving it physically

This commit is contained in:
Alexey Sedunov
2015-04-28 12:34:58 +03:00
parent 6ee59a06dd
commit 8879166f53
43 changed files with 498 additions and 42 deletions
@@ -21,17 +21,20 @@ import com.intellij.psi.PsiElement;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.psi.psiUtil.PsiUtilPackage;
import org.jetbrains.kotlin.psi.stubs.KotlinPlaceHolderStub;
import org.jetbrains.kotlin.psi.stubs.elements.JetStubElementTypes;
import org.jetbrains.kotlin.lexer.JetTokens;
import org.jetbrains.kotlin.name.FqName;
import org.jetbrains.kotlin.name.Name;
import org.jetbrains.kotlin.name.SpecialNames;
import org.jetbrains.kotlin.psi.psiUtil.PsiUtilPackage;
import org.jetbrains.kotlin.psi.stubs.KotlinPlaceHolderStub;
import org.jetbrains.kotlin.psi.stubs.elements.JetStubElementTypes;
import java.util.Collections;
import java.util.List;
public class JetPackageDirective extends JetModifierListOwnerStub<KotlinPlaceHolderStub<JetPackageDirective>> implements JetReferenceExpression {
public class JetPackageDirective
extends JetModifierListOwnerStub<KotlinPlaceHolderStub<JetPackageDirective>>
implements JetReferenceExpression {
private String qualifiedNameCache = null;
public JetPackageDirective(@NotNull ASTNode node) {
@@ -116,6 +119,29 @@ public class JetPackageDirective extends JetModifierListOwnerStub<KotlinPlaceHol
return new FqName(getQualifiedNameOf(nameExpression));
}
public void setFqName(@NotNull FqName fqName) {
if (fqName.isRoot()) {
delete();
return;
}
JetPsiFactory psiFactory = new JetPsiFactory(getProject());
PsiElement newExpression = psiFactory.createExpression(fqName.asString());
JetExpression currentExpression = getPackageNameExpression();
if (currentExpression != null) {
currentExpression.replace(newExpression);
return;
}
PsiElement keyword = getPackageKeyword();
if (keyword != null) {
addAfter(newExpression, keyword);
return;
}
replace(psiFactory.createPackageDirective(fqName));
}
@NotNull
public String getQualifiedName() {
if (qualifiedNameCache == null) {
@@ -139,6 +165,11 @@ public class JetPackageDirective extends JetModifierListOwnerStub<KotlinPlaceHol
return builder.toString();
}
@Nullable
public PsiElement getPackageKeyword() {
return findChildByType(JetTokens.PACKAGE_KEYWORD);
}
@Override
public void subtreeChanged() {
qualifiedNameCache = null;
@@ -284,8 +284,12 @@ public class JetPsiFactory(private val project: Project) {
return stringTemplateExpression.getEntries()[0] as JetStringTemplateEntryWithExpression
}
public fun createPackageDirective(fqName: FqName): JetPackageDirective {
return createFile("package ${fqName.asString()}").getPackageDirective()
}
public fun createPackageDirectiveIfNeeded(fqName: FqName): JetPackageDirective? {
return if (fqName.isRoot()) null else createFile("package ${fqName.asString()}").getPackageDirective()
return if (fqName.isRoot()) null else createPackageDirective(fqName)
}
public fun createImportDirective(path: String): JetImportDirective {
@@ -41,6 +41,17 @@ private var Project.elementsToShorten: MutableSet<ShorteningRequest>?
public var Project.ensureElementsToShortenIsEmptyBeforeRefactoring: Boolean
by NotNullableUserDataProperty(Key.create("ENSURE_ELEMENTS_TO_SHORTEN_IS_EMPTY"), true)
public fun Project.runWithElementsToShortenIsEmptyIgnored(action: () -> Unit) {
val ensureElementsToShortenIsEmpty = ensureElementsToShortenIsEmptyBeforeRefactoring
try {
ensureElementsToShortenIsEmptyBeforeRefactoring = false
action()
} finally {
ensureElementsToShortenIsEmptyBeforeRefactoring = ensureElementsToShortenIsEmpty
}
}
private fun Project.getOrCreateElementsToShorten(): MutableSet<ShorteningRequest> {
var elements = elementsToShorten
if (elements == null) {
@@ -0,0 +1,7 @@
<spot>package bar.foo</spot>
class MyClass: OtherClass {
fun myFun() {
}
}
@@ -0,0 +1,9 @@
<spot>package foo.bar</spot>
import bar.foo.OtherClass
class MyClass: OtherClass {
fun myFun() {
}
}
@@ -0,0 +1,6 @@
<html>
<body>
This intention changes qualified package name of the current file without moving the file itself.
All references and import directives are updated accordingly.
</body>
</html>
+5
View File
@@ -848,6 +848,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.refactoring.move.changePackage.ChangePackageIntention</className>
<category>Kotlin</category>
</intentionAction>
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.ExplicitGetInspection"
displayName="Explicit 'get'"
groupName="Kotlin"
@@ -0,0 +1,74 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.refactoring.move.changePackage;
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.codeInsight.template.*
import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.Disposer
import com.intellij.psi.PsiDocumentManager
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.refactoring.canRefactor
import org.jetbrains.kotlin.idea.intentions.JetSelfTargetingOffsetIndependentIntention
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.JetPackageDirective
public class ChangePackageIntention: JetSelfTargetingOffsetIndependentIntention<JetPackageDirective>(javaClass(), "Change package") {
override fun isApplicableTo(element: JetPackageDirective) = element.getPackageNameExpression() != null
override fun applyTo(element: JetPackageDirective, editor: Editor) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
throw UnsupportedOperationException("Do not call applyTo() in the test mode")
}
val file = element.getContainingJetFile()
val project = file.getProject()
val nameExpression = element.getPackageNameExpression()!!
val currentName = element.getQualifiedName()
val builder = TemplateBuilderImpl(file)
builder.replaceElement(
nameExpression,
object: Expression() {
override fun calculateQuickResult(context: ExpressionContext?) = TextResult(currentName)
override fun calculateResult(context: ExpressionContext?) = TextResult(currentName)
override fun calculateLookupItems(context: ExpressionContext?) = arrayOf(LookupElementBuilder.create(currentName))
}
)
editor.getCaretModel().moveToOffset(0)
TemplateManager.getInstance(project).startTemplate(
editor,
builder.buildInlineTemplate(),
object: TemplateEditingAdapter() {
override fun templateFinished(template: Template?, brokenOff: Boolean) {
if (!brokenOff) {
// Restore original name and run refactoring
val packageDirective = file.getPackageDirective()!!
val newFqName = packageDirective.getFqName()
packageDirective.setFqName(FqName(currentName))
KotlinChangePackageRefactoring(file).run(newFqName)
}
}
}
)
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.refactoring.move.changePackage
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.PackageWrapper
import org.jetbrains.kotlin.idea.codeInsight.shorten.runWithElementsToShortenIsEmptyIgnored
import org.jetbrains.kotlin.idea.refactoring.move.PackageNameInfo
import org.jetbrains.kotlin.idea.refactoring.move.getInternalReferencesToUpdateOnPackageNameChange
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.KotlinMoveTarget
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveKotlinTopLevelDeclarationsOptions
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveKotlinTopLevelDeclarationsProcessor
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.Mover
import org.jetbrains.kotlin.idea.refactoring.move.postProcessMoveUsages
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.psi.JetNamedDeclaration
public class KotlinChangePackageRefactoring(val file: JetFile) {
private val project = file.getProject()
fun run(newFqName: FqName) {
val packageDirective = file.getPackageDirective() ?: return
val currentFqName = packageDirective.getFqName()
val declarationProcessor = MoveKotlinTopLevelDeclarationsProcessor(
project,
MoveKotlinTopLevelDeclarationsOptions(
elementsToMove = file.getDeclarations().filterIsInstance<JetNamedDeclaration>(),
moveTarget = object: KotlinMoveTarget {
override val packageWrapper = PackageWrapper(file.getManager(), newFqName.asString())
override fun getOrCreateTargetPsi(originalPsi: PsiElement) = originalPsi.getContainingFile()
override fun getTargetPsiIfExists(originalPsi: PsiElement) = null
override fun verify(file: PsiFile) = null
},
updateInternalReferences = false
),
Mover.Idle // we don't need to move any declarations physically
)
val declarationUsages = declarationProcessor.findUsages().toList()
val internalUsages = file.getInternalReferencesToUpdateOnPackageNameChange(PackageNameInfo(currentFqName, newFqName))
project.executeWriteCommand("Change file's package to '${newFqName.asString()}'") {
postProcessMoveUsages(internalUsages)
packageDirective.setFqName(newFqName)
project.runWithElementsToShortenIsEmptyIgnored { declarationProcessor.execute(declarationUsages) }
}
}
}
@@ -16,27 +16,26 @@
package org.jetbrains.kotlin.idea.refactoring.move.moveFilesOrDirectories
import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFileHandler
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.usageView.UsageInfo
import com.intellij.openapi.roots.JavaProjectRootsUtil
import com.intellij.psi.PsiCompiledElement
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.psi.JetNamedDeclaration
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.idea.references.JetSimpleNameReference
import org.jetbrains.kotlin.psi.psiUtil.getPackage
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFileHandler
import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil
import com.intellij.usageView.UsageInfo
import org.jetbrains.kotlin.idea.codeInsight.shorten.runWithElementsToShortenIsEmptyIgnored
import org.jetbrains.kotlin.idea.refactoring.move.PackageNameInfo
import org.jetbrains.kotlin.idea.refactoring.move.getInternalReferencesToUpdateOnPackageNameChange
import org.jetbrains.kotlin.idea.refactoring.move.postProcessMoveUsages
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveKotlinTopLevelDeclarationsProcessor
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveKotlinTopLevelDeclarationsOptions
import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.DeferredJetFileKotlinMoveTarget
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveKotlinTopLevelDeclarationsOptions
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveKotlinTopLevelDeclarationsProcessor
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.Mover
import org.jetbrains.kotlin.idea.codeInsight.shorten.ensureElementsToShortenIsEmptyBeforeRefactoring
import org.jetbrains.kotlin.idea.refactoring.move.postProcessMoveUsages
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.psi.JetNamedDeclaration
import org.jetbrains.kotlin.psi.psiUtil.getPackage
public class MoveKotlinFileHandler : MoveFileHandler() {
private var packageNameInfo: PackageNameInfo? = null
@@ -82,10 +81,7 @@ public class MoveKotlinFileHandler : MoveFileHandler() {
},
updateInternalReferences = false
),
object: Mover {
[suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")]
override fun invoke(originalElement: JetNamedDeclaration, targetFile: JetFile): JetNamedDeclaration = originalElement
}
Mover.Idle
)
this.packageNameInfo = packageNameInfo
@@ -100,29 +96,20 @@ public class MoveKotlinFileHandler : MoveFileHandler() {
override fun updateMovedFile(file: PsiFile) {
if (file !is JetFile) return
val packageNameInfo = packageNameInfo ?: return
val packageNameInfo = packageNameInfo
if (packageNameInfo == null) return
val usages = file.getInternalReferencesToUpdateOnPackageNameChange(packageNameInfo)
postProcessMoveUsages(usages)
val packageRef = file.getPackageDirective()?.getLastReferenceExpression()?.getReference() as? JetSimpleNameReference
packageRef?.bindToFqName(packageNameInfo.newPackageName)
postProcessMoveUsages(file.getInternalReferencesToUpdateOnPackageNameChange(packageNameInfo))
file.getPackageDirective()?.setFqName(packageNameInfo.newPackageName)
}
override fun retargetUsages(usageInfos: List<UsageInfo>?, oldToNewMap: Map<PsiElement, PsiElement>?) {
val processor = declarationMoveProcessor ?: return
val project = processor.project
val ensureElementsToShortenIsEmpty = project.ensureElementsToShortenIsEmptyBeforeRefactoring
try {
project.ensureElementsToShortenIsEmptyBeforeRefactoring = false
usageInfos?.let { processor.execute(it) }
} finally {
project.ensureElementsToShortenIsEmptyBeforeRefactoring = ensureElementsToShortenIsEmpty
clearState()
processor.project.runWithElementsToShortenIsEmptyIgnored {
try {
usageInfos?.let { processor.execute(it) }
} finally {
clearState()
}
}
}
}
@@ -72,6 +72,11 @@ trait Mover: (originalElement: JetNamedDeclaration, targetFile: JetFile) -> JetN
return newElement
}
}
object Idle: Mover {
[suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")]
override fun invoke(originalElement: JetNamedDeclaration, targetFile: JetFile) = originalElement
}
}
public class MoveKotlinTopLevelDeclarationsOptions(
@@ -0,0 +1,5 @@
class Foo
fun foo() {
}
@@ -0,0 +1,6 @@
class Test {
static void test() {
new Foo();
_DefaultPackage.foo();
}
}
@@ -0,0 +1,4 @@
fun test() {
Foo()
foo()
}
@@ -0,0 +1,8 @@
import static _DefaultPackage.foo;
class Test {
static void test() {
new Foo();
foo();
}
}
@@ -0,0 +1,7 @@
import Foo
import foo
fun test() {
Foo()
foo()
}
@@ -0,0 +1,7 @@
package source
class Foo
fun foo() {
}
@@ -0,0 +1,6 @@
class Test {
static void test() {
new source.Foo();
source.SourcePackage.foo();
}
}
@@ -0,0 +1,4 @@
fun test() {
source.Foo()
source.foo()
}
@@ -0,0 +1,9 @@
import source.Foo;
import static source.SourcePackage.foo;
class Test {
static void test() {
new Foo();
foo();
}
}
@@ -0,0 +1,7 @@
import source.Foo
import source.foo
fun test() {
Foo()
foo()
}
@@ -0,0 +1,5 @@
{
"mainFile": "source/test.kt",
"type": "CHANGE_PACKAGE_DIRECTIVE",
"newPackageName": ""
}
@@ -0,0 +1,7 @@
package target
class Foo
fun foo() {
}
@@ -0,0 +1,8 @@
package target;
class Test {
static void test() {
new Foo();
TargetPackage.foo();
}
}
@@ -0,0 +1,6 @@
package target
fun test() {
Foo()
foo()
}
@@ -0,0 +1,10 @@
package target;
import static target.TargetPackage.foo;
class Test {
static void test() {
new Foo();
foo();
}
}
@@ -0,0 +1,9 @@
package target
import target.Foo
import target.foo
fun test() {
Foo()
foo()
}
@@ -0,0 +1,11 @@
package usages;
import target.Foo;
import target.TargetPackage;
class Test {
static void test() {
new Foo();
TargetPackage.foo();
}
}
@@ -0,0 +1,9 @@
package usages
import target.Foo
import target.foo
fun test() {
Foo()
foo()
}
@@ -0,0 +1,11 @@
package usages;
import target.Foo;
import static target.TargetPackage.foo;
class Test {
static void test() {
new Foo();
foo();
}
}
@@ -0,0 +1,9 @@
package usages
import target.Foo
import target.foo
fun test() {
Foo()
foo()
}
@@ -0,0 +1,7 @@
package source
class Foo
fun foo() {
}
@@ -0,0 +1,8 @@
package target;
class Test {
static void test() {
new source.Foo();
source.SourcePackage.foo();
}
}
@@ -0,0 +1,6 @@
package target
fun test() {
source.Foo()
source.foo()
}
@@ -0,0 +1,11 @@
package target;
import source.Foo;
import static source.SourcePackage.foo;
class Test {
static void test() {
new Foo();
foo();
}
}
@@ -0,0 +1,9 @@
package target
import source.Foo
import source.foo
fun test() {
Foo()
foo()
}
@@ -0,0 +1,8 @@
package usages;
class Test {
static void test() {
new source.Foo();
source.SourcePackage.foo();
}
}
@@ -0,0 +1,6 @@
package usages
fun test() {
source.Foo()
source.foo()
}
@@ -0,0 +1,11 @@
package usages;
import source.Foo;
import static source.SourcePackage.foo;
class Test {
static void test() {
new Foo();
foo();
}
}
@@ -0,0 +1,9 @@
package usages
import source.Foo
import source.foo
fun test() {
Foo()
foo()
}
@@ -0,0 +1,5 @@
{
"mainFile": "source/test.kt",
"type": "CHANGE_PACKAGE_DIRECTIVE",
"newPackageName": "target"
}
@@ -37,6 +37,7 @@ import com.intellij.refactoring.move.moveInner.MoveInnerProcessor
import com.intellij.refactoring.move.moveMembers.MockMoveMembersOptions
import com.intellij.refactoring.move.moveMembers.MoveMembersProcessor
import com.intellij.util.ActionRunner
import org.jetbrains.kotlin.idea.refactoring.move.changePackage.KotlinChangePackageRefactoring
import org.jetbrains.kotlin.idea.test.KotlinMultiFileTestCase
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.JetFileKotlinMoveTarget
@@ -49,6 +50,7 @@ import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.psi.JetNamedDeclaration
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.test.JetTestUtils
import java.io.File
@@ -269,5 +271,11 @@ enum class MoveAction {
}
}
CHANGE_PACKAGE_DIRECTIVE {
override fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementAtCaret: PsiElement?, config: JsonObject) {
KotlinChangePackageRefactoring(mainFile as JetFile).run(FqName(config.getString("newPackageName")))
}
}
abstract fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementAtCaret: PsiElement?, config: JsonObject)
}
@@ -209,6 +209,18 @@ public class JetMoveTestGenerated extends AbstractJetMoveTest {
doTest(fileName);
}
@TestMetadata("kotlin/changePackage/changeToDefaultPackage/changeToDefaultPackage.test")
public void testKotlin_changePackage_changeToDefaultPackage_ChangeToDefaultPackage() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/move/kotlin/changePackage/changeToDefaultPackage/changeToDefaultPackage.test");
doTest(fileName);
}
@TestMetadata("kotlin/changePackage/changeToNonDefaultPackage/changeToNonDefaultPackage.test")
public void testKotlin_changePackage_changeToNonDefaultPackage_ChangeToNonDefaultPackage() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/move/kotlin/changePackage/changeToNonDefaultPackage/changeToNonDefaultPackage.test");
doTest(fileName);
}
@TestMetadata("kotlin/moveFile/internalReferences/internalReferences.test")
public void testKotlin_moveFile_internalReferences_InternalReferences() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/move/kotlin/moveFile/internalReferences/internalReferences.test");