Hint action to update usages on cut/paste of top-level declarations

This commit is contained in:
Valentin Kipyatkov
2017-03-19 16:51:25 +03:00
parent cb5459b7d7
commit 40bbf82a41
31 changed files with 759 additions and 10 deletions
+5
View File
@@ -34,6 +34,10 @@
<component>
<implementation-class>org.jetbrains.kotlin.idea.highlighter.KotlinBeforeResolveHighlightingPass$Factory</implementation-class>
</component>
<component>
<implementation-class>org.jetbrains.kotlin.idea.refactoring.cutPaste.MoveDeclarationsPassFactory</implementation-class>
<skipForDefaultProject/>
</component>
<component>
<implementation-class>org.jetbrains.kotlin.idea.completion.LookupCancelWatcher</implementation-class>
</component>
@@ -582,6 +586,7 @@
<copyPastePostProcessor implementation="org.jetbrains.kotlin.idea.conversion.copy.ConvertTextJavaCopyPasteProcessor"/>
<copyPastePostProcessor implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinCopyPasteReferenceProcessor"/>
<copyPastePreProcessor implementation="org.jetbrains.kotlin.idea.editor.KotlinLiteralCopyPasteProcessor"/>
<copyPastePostProcessor implementation="org.jetbrains.kotlin.idea.refactoring.cutPaste.MoveDeclarationsCopyPasteProcessor"/>
<breadcrumbsInfoProvider implementation="org.jetbrains.kotlin.idea.codeInsight.KotlinBreadcrumbsInfoProvider"/>
@@ -66,7 +66,7 @@ class ChangeMemberFunctionSignatureFix private constructor(
companion object {
private val SIGNATURE_SOURCE_RENDERER = IdeDescriptorRenderers.SOURCE_CODE.withOptions {
renderDefaultValues = false
defaultParameterValueRenderer = null
}
private val SIGNATURE_PREVIEW_RENDERER = DescriptorRenderer.withOptions {
@@ -75,7 +75,7 @@ class ChangeMemberFunctionSignatureFix private constructor(
modifiers = emptySet()
classifierNamePolicy = ClassifierNamePolicy.SHORT
unitReturnType = false
renderDefaultValues = false
defaultParameterValueRenderer = null
}
}
}
@@ -84,7 +84,7 @@ class ReplaceProtectedToPublishedApiCallFix(
companion object : KotlinSingleIntentionActionFactory() {
val signatureRenderer = IdeDescriptorRenderers.SOURCE_CODE.withOptions {
renderDefaultValues = false
defaultParameterValueRenderer = null
startFromDeclarationKeyword = true
withoutReturnType = true
}
@@ -0,0 +1,108 @@
/*
* Copyright 2010-2017 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.cutPaste
import com.intellij.codeInsight.editorActions.CopyPastePostProcessor
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiModificationTracker
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.psiUtil.elementsInRange
import java.awt.datatransfer.Transferable
class MoveDeclarationsCopyPasteProcessor : CopyPastePostProcessor<MoveDeclarationsTransferableData>() {
companion object {
private val LOG = Logger.getInstance(MoveDeclarationsCopyPasteProcessor::class.java)
fun rangeToDeclarations(file: KtFile, startOffset: Int, endOffset: Int): List<KtNamedDeclaration> {
val elementsInRange = file.elementsInRange(TextRange(startOffset, endOffset))
val meaningfulElements = elementsInRange.filterNot { it is PsiWhiteSpace || it is PsiComment }
if (meaningfulElements.isEmpty()) return emptyList()
if (!meaningfulElements.all { it is KtNamedDeclaration }) return emptyList()
@Suppress("UNCHECKED_CAST")
return meaningfulElements as List<KtNamedDeclaration>
}
}
override fun collectTransferableData(
file: PsiFile,
editor: Editor,
startOffsets: IntArray,
endOffsets: IntArray
): List<MoveDeclarationsTransferableData> {
if (file !is KtFile) return emptyList()
if (startOffsets.size != 1) return emptyList()
val declarations = rangeToDeclarations(file, startOffsets[0], endOffsets[0])
if (declarations.isEmpty() || declarations.any { it.parent !is KtFile }) return emptyList()
if (declarations.any { it.name == null }) return emptyList()
val declarationNames = declarations.map { it.name!! }.toSet()
val stubTexts = declarations.map { MoveDeclarationsTransferableData.STUB_RENDERER.render(it.resolveToDescriptor()) }
return listOf(MoveDeclarationsTransferableData(file.virtualFile.url, stubTexts, declarationNames))
}
override fun extractTransferableData(content: Transferable): List<MoveDeclarationsTransferableData> {
try {
if (content.isDataFlavorSupported(MoveDeclarationsTransferableData.DATA_FLAVOR)) {
return listOf(content.getTransferData(MoveDeclarationsTransferableData.DATA_FLAVOR) as MoveDeclarationsTransferableData)
}
}
catch (e: Throwable) {
LOG.error(e)
}
return emptyList()
}
override fun processTransferableData(
project: Project,
editor: Editor,
bounds: RangeMarker,
caretOffset: Int,
indented: Ref<Boolean>,
values: List<MoveDeclarationsTransferableData>
) {
val data = values.single()
fun putCookie() {
if (bounds.isValid) {
val cookie = MoveDeclarationsEditorCookie(data, bounds, PsiModificationTracker.SERVICE.getInstance(project).modificationCount)
editor.putUserData(MoveDeclarationsEditorCookie.KEY, cookie)
}
}
if (ApplicationManager.getApplication().isUnitTestMode) {
putCookie()
}
else {
// in real application we put cookie later to allow all other paste handlers do their work (because modificationCount will change)
ApplicationManager.getApplication().invokeLater(::putCookie)
}
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2017 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.cutPaste
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.util.Key
class MoveDeclarationsEditorCookie(
val data: MoveDeclarationsTransferableData,
val bounds: RangeMarker,
val modificationCount: Long
) {
companion object {
val KEY = Key<MoveDeclarationsEditorCookie>("MoveDeclarationsEditorCookie")
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2017 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.cutPaste
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInspection.HintAction
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.keymap.KeymapUtil
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.refactoring.BaseRefactoringIntentionAction
import org.jetbrains.kotlin.idea.conversion.copy.range
class MoveDeclarationsIntentionAction(
private val processor: MoveDeclarationsProcessor,
private val bounds: RangeMarker,
private val modificationCount: Long
) : BaseRefactoringIntentionAction(), HintAction {
override fun startInWriteAction() = false
override fun getText() = "Update usages to reflect package name change"
override fun getFamilyName() = "Update usages on declarations cut/paste"
override fun isAvailable(project: Project, editor: Editor, element: PsiElement): Boolean {
return PsiModificationTracker.SERVICE.getInstance(processor.project).modificationCount == modificationCount
}
override fun invoke(project: Project, editor: Editor, element: PsiElement) {
processor.performRefactoring()
}
override fun showHint(editor: Editor): Boolean {
val range = bounds.range ?: return false
if (editor.caretModel.offset != range.endOffset) return false
if (PsiModificationTracker.SERVICE.getInstance(processor.project).modificationCount != modificationCount) return false
val hintText = "$text? ${KeymapUtil.getFirstKeyboardShortcutText(ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS))}"
HintManager.getInstance().showQuestionHint(editor, hintText, range.endOffset, range.endOffset) {
processor.performRefactoring()
true
}
return true
}
}
@@ -0,0 +1,79 @@
/*
* Copyright 2010-2017 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.cutPaste
import com.intellij.codeHighlighting.Pass
import com.intellij.codeHighlighting.TextEditorHighlightingPass
import com.intellij.codeHighlighting.TextEditorHighlightingPassFactory
import com.intellij.codeHighlighting.TextEditorHighlightingPassRegistrar
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfoType
import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil
import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixAction
import com.intellij.openapi.components.AbstractProjectComponent
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiModificationTracker
import org.jetbrains.kotlin.idea.conversion.copy.range
class MoveDeclarationsPassFactory(project: Project, highlightingPassRegistrar: TextEditorHighlightingPassRegistrar)
: AbstractProjectComponent(project), TextEditorHighlightingPassFactory {
init {
highlightingPassRegistrar.registerTextEditorHighlightingPass(this, TextEditorHighlightingPassRegistrar.Anchor.BEFORE, Pass.POPUP_HINTS, true, true)
}
override fun createHighlightingPass(file: PsiFile, editor: Editor): TextEditorHighlightingPass? {
return MyPass(file.project, file, editor)
}
private class MyPass(
private val project: Project,
private val file: PsiFile,
private val editor: Editor
) : TextEditorHighlightingPass(project, editor.document, true) {
override fun doCollectInformation(progress: ProgressIndicator) {}
override fun doApplyInformationToEditor() {
val info = buildHighlightingInfo()
UpdateHighlightersUtil.setHighlightersToEditor(project, myDocument!!, 0, file.textLength, listOfNotNull(info), colorsScheme, id)
}
private fun buildHighlightingInfo(): HighlightInfo? {
val cookie = editor.getUserData(MoveDeclarationsEditorCookie.KEY) ?: return null
if (cookie.modificationCount != PsiModificationTracker.SERVICE.getInstance(project).modificationCount) return null
val processor = MoveDeclarationsProcessor.build(editor, cookie)
if (processor == null) {
editor.putUserData(MoveDeclarationsEditorCookie.KEY, null)
return null
}
val info = HighlightInfo.newHighlightInfo(HighlightInfoType.INFORMATION)
.range(cookie.bounds.range!!)
.createUnconditionally()
QuickFixAction.registerQuickFixAction(info, MoveDeclarationsIntentionAction(processor, cookie.bounds, cookie.modificationCount))
return info
}
}
}
@@ -0,0 +1,134 @@
/*
* Copyright 2010-2017 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.cutPaste
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.codeInsight.shorten.runRefactoringAndKeepDelayedRequests
import org.jetbrains.kotlin.idea.conversion.copy.range
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.getSourceRoot
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.psiUtil.startOffset
class MoveDeclarationsProcessor(
val project: Project,
private val sourcePsiFile: KtFile,
private val targetPsiFile: KtFile,
private val pastedDeclarations: List<KtNamedDeclaration>,
private val stubTexts: List<String>
) {
companion object {
fun build(editor: Editor, cookie: MoveDeclarationsEditorCookie): MoveDeclarationsProcessor? {
val data = cookie.data
val project = editor.project ?: return null
val range = cookie.bounds.range ?: return null
val sourceFileUrl = data.sourceFileUrl
val sourceFile = VirtualFileManager.getInstance().findFileByUrl(sourceFileUrl) ?: return null
if (sourceFile.getSourceRoot(project) == null) return null
val psiDocumentManager = PsiDocumentManager.getInstance(project)
psiDocumentManager.commitAllDocuments()
val targetPsiFile = psiDocumentManager.getPsiFile(editor.document) as? KtFile ?: return null
if (targetPsiFile.virtualFile.getSourceRoot(project) == null) return null
val sourcePsiFile = PsiManager.getInstance(project).findFile(sourceFile) as? KtFile ?: return null
if (targetPsiFile == sourcePsiFile) return null
val declarations = MoveDeclarationsCopyPasteProcessor.rangeToDeclarations(targetPsiFile, range.startOffset, range.endOffset)
if (declarations.isEmpty() || declarations.any { it.parent !is KtFile }) return null
if (sourcePsiFile.packageFqName == targetPsiFile.packageFqName) return null
// check that declarations were cut (not copied)
val filteredDeclarations = sourcePsiFile.declarations.filter { it.name in data.declarationNames }
val stubs = data.stubTexts.toSet()
if (filteredDeclarations.any { MoveDeclarationsTransferableData.STUB_RENDERER.render(it.resolveToDescriptor()) in stubs }) return null
return MoveDeclarationsProcessor(project, sourcePsiFile, targetPsiFile, declarations, data.stubTexts)
}
}
private val psiDocumentManager = PsiDocumentManager.getInstance(project)
private val sourceDocument = psiDocumentManager.getDocument(sourcePsiFile)!!
fun performRefactoring() {
psiDocumentManager.commitAllDocuments()
val commandName = "Usage update"
val commandGroupId = Any() // we need to group both commands for undo
val insertedRange = project.executeWriteCommand<RangeMarker>(commandName, commandGroupId) {
//TODO: can stub declarations interfere with pasted declarations? I could not find such cases
insertStubDeclarations()
}
psiDocumentManager.commitDocument(sourceDocument)
val stubDeclarations = MoveDeclarationsCopyPasteProcessor.rangeToDeclarations(sourcePsiFile, insertedRange.startOffset, insertedRange.endOffset)
assert(stubDeclarations.size == pastedDeclarations.size) //TODO: can they ever differ?
val mover = object: Mover {
override fun invoke(declaration: KtNamedDeclaration, targetContainer: KtElement): KtNamedDeclaration {
val index = stubDeclarations.indexOf(declaration)
assert(index >= 0)
declaration.delete()
return pastedDeclarations[index]
}
}
val declarationProcessor = MoveKotlinDeclarationsProcessor(
MoveDeclarationsDescriptor(
elementsToMove = stubDeclarations,
moveTarget = KotlinMoveTargetForExistingElement(targetPsiFile),
delegate = MoveDeclarationsDelegate.TopLevel,
project = project
),
mover
)
val declarationUsages = declarationProcessor.findUsages().toList()
// val changeInfo = ContainerChangeInfo(ContainerInfo.Package(sourcePackageName), ContainerInfo.Package(targetPackageName))
// val internalUsages = file.getInternalReferencesToUpdateOnPackageNameChange(changeInfo)
project.executeWriteCommand(commandName, commandGroupId) {
// postProcessMoveUsages(internalUsages) //TODO?
project.runRefactoringAndKeepDelayedRequests { declarationProcessor.execute(declarationUsages) }
psiDocumentManager.doPostponedOperationsAndUnblockDocument(sourceDocument)
assert(insertedRange.isValid)
sourceDocument.deleteString(insertedRange.startOffset, insertedRange.endOffset)
}
}
private fun insertStubDeclarations(): RangeMarker {
val insertionOffset = sourcePsiFile.declarations.firstOrNull()?.startOffset ?: sourcePsiFile.textLength
val textToInsert = "\n//start\n\n" + stubTexts.joinToString(separator = "\n") + "\n//end\n"
sourceDocument.insertString(insertionOffset, textToInsert)
return sourceDocument.createRangeMarker(TextRange(insertionOffset, insertionOffset + textToInsert.length))
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2017 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.cutPaste
import com.intellij.codeInsight.editorActions.TextBlockTransferableData
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import java.awt.datatransfer.DataFlavor
//TODO: how to make it work inside same project only?
class MoveDeclarationsTransferableData(
val sourceFileUrl: String,
val stubTexts: List<String>,
val declarationNames: Set<String>
) : TextBlockTransferableData {
override fun getFlavor() = DATA_FLAVOR
override fun getOffsetCount() = 0
override fun getOffsets(offsets: IntArray?, index: Int) = index
override fun setOffsets(offsets: IntArray?, index: Int) = index
companion object {
val DATA_FLAVOR = DataFlavor(MoveDeclarationsCopyPasteProcessor::class.java, "class: MoveDeclarationsCopyPasteProcessor")
val STUB_RENDERER = IdeDescriptorRenderers.SOURCE_CODE.withOptions {
defaultParameterValueRenderer = { "xxx" } // we need default value to be parsed as expression
}
}
}