Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REF: introduce constant refactoring #4985

Merged
merged 1 commit into from
May 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.intellij.lang.refactoring.RefactoringSupportProvider
import com.intellij.psi.PsiElement
import com.intellij.refactoring.RefactoringActionHandler
import org.rust.ide.refactoring.extractFunction.RsExtractFunctionHandler
import org.rust.ide.refactoring.introduceConstant.RsIntroduceConstantHandler
import org.rust.ide.refactoring.introduceParameter.RsIntroduceParameterHandler
import org.rust.ide.refactoring.introduceVariable.RsIntroduceVariableHandler
import org.rust.lang.core.macros.isExpandedFromMacro
Expand All @@ -25,6 +26,8 @@ class RsRefactoringSupportProvider : RefactoringSupportProvider() {
override fun getIntroduceVariableHandler(element: PsiElement?): RefactoringActionHandler =
RsIntroduceVariableHandler()

override fun getIntroduceConstantHandler(): RefactoringActionHandler? = RsIntroduceConstantHandler()

override fun getExtractMethodHandler(): RefactoringActionHandler = RsExtractFunctionHandler()

override fun getIntroduceParameterHandler(): RefactoringActionHandler = RsIntroduceParameterHandler()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import org.rust.lang.core.psi.ext.*

class RsExtractFunctionHandler : RefactoringActionHandler {
override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext?) {
//this doesn't get called form the editor.
//this doesn't get called from the editor.
}

override fun invoke(project: Project, editor: Editor?, file: PsiFile?, dataContext: DataContext?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust.ide.refactoring.introduceConstant

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiParserFacade
import com.intellij.refactoring.RefactoringActionHandler
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.util.CommonRefactoringUtil
import org.rust.ide.inspections.import.RsImportHelper
import org.rust.ide.refactoring.*
import org.rust.lang.core.psi.*
import org.rust.lang.core.psi.ext.RsElement
import org.rust.openapiext.runWriteCommandAction

class RsIntroduceConstantHandler : RefactoringActionHandler {
override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext) {
if (file !is RsFile) return
val exprs = findCandidateExpressionsToExtract(editor, file).filter { it.isExtractable() }

when (exprs.size) {
0 -> {
val message = RefactoringBundle.message(if (editor.selectionModel.hasSelection())
"selected.block.should.represent.an.expression"
else
"refactoring.introduce.selection.error"
)
val title = RefactoringBundle.message("introduce.constant.title")
val helpId = "refactoring.extractConstant"
CommonRefactoringUtil.showErrorHint(project, editor, message, title, helpId)
}
1 -> extractExpression(editor, exprs.single())
else -> {
showExpressionChooser(editor, exprs) {
extractExpression(editor, it)
}
}
}
}

override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext?) {
//this doesn't get called from the editor.
}
}

private fun RsExpr.isExtractable(): Boolean {
return when (this) {
is RsLitExpr -> true
is RsBinaryExpr -> this.left.isExtractable() && (this.right?.isExtractable() ?: true)
else -> false
}
}

private fun replaceWithConstant(expr: RsExpr, occurrences: List<RsExpr>, candidate: InsertionCandidate, editor: Editor) {
val project = expr.project
val factory = RsPsiFactory(project)
val suggestedNames = expr.suggestedNames()
val name = suggestedNames.default.toUpperCase()
val const = factory.createConstant(name, expr)

val (insertedConstant, replaced) = project.runWriteCommandAction {
val newline = PsiParserFacade.SERVICE.getInstance(project).createWhiteSpaceFromText("\n")
val context = candidate.parent
val inserted = context.addBefore(const, candidate.anchor) as RsConstant
context.addAfter(newline, inserted)
val replaced = occurrences.map {
val created = factory.createExpression(name)
val element = it.replace(created) as RsElement
RsImportHelper.importElements(element, setOf(inserted))
element
}
Pair(inserted, replaced.toList())
}

editor.caretModel.moveToOffset(insertedConstant.identifier?.textRange?.startOffset
?: error("Impossible because we just created a constant with a name"))

PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
RsInPlaceVariableIntroducer(insertedConstant, editor, project, "Choose a constant name", replaced)
.performInplaceRefactoring(LinkedHashSet(suggestedNames.all.map { it.toUpperCase() }))
}

private fun extractExpression(editor: Editor, expr: RsExpr) {
if (!expr.isValid) return
val occurrences = findOccurrences(expr)
showOccurrencesChooser(editor, expr, occurrences) { occurrencesToReplace ->
showInsertionChooser(editor, expr) {
replaceWithConstant(expr, occurrencesToReplace, it, editor)
}
}
}
149 changes: 149 additions & 0 deletions src/main/kotlin/org/rust/ide/refactoring/introduceConstant/ui.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Use of this source code is governed by the MIT license that can be
* found in the LICENSE file.
*/

package org.rust.ide.refactoring.introduceConstant

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.MarkupModel
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.JBPopupListener
import com.intellij.openapi.ui.popup.LightweightWindowEvent
import com.intellij.openapiext.isUnitTestMode
import com.intellij.psi.PsiElement
import org.jetbrains.annotations.TestOnly
import org.rust.lang.core.psi.RsExpr
import org.rust.lang.core.psi.RsFile
import org.rust.lang.core.psi.RsFunction
import org.rust.lang.core.psi.RsModItem
import org.rust.lang.core.psi.ext.block
import java.awt.Component
import javax.swing.DefaultListCellRenderer
import javax.swing.JList

class Highlighter(private val editor: Editor) : JBPopupListener {
private var highlighter: RangeHighlighter? = null
private val attributes = EditorColorsManager.getInstance().globalScheme.getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES)

fun onSelect(candidate: InsertionCandidate) {
dropHighlighter()
val markupModel: MarkupModel = editor.markupModel

val textRange = candidate.parent.textRange
highlighter = markupModel.addRangeHighlighter(
textRange.startOffset, textRange.endOffset, HighlighterLayer.SELECTION - 1, attributes,
HighlighterTargetArea.EXACT_RANGE)
}

override fun onClosed(event: LightweightWindowEvent) {
dropHighlighter()
}

private fun dropHighlighter() {
highlighter?.dispose()
}
}

fun showInsertionChooser(
editor: Editor,
expr: RsExpr,
callback: (InsertionCandidate) -> Unit
) {
val candidates = findInsertionCandidates(expr)
if (isUnitTestMode) {
callback(MOCK!!.chooseInsertionPoint(expr, candidates))
} else {
val highlighter = Highlighter(editor)
JBPopupFactory.getInstance()
.createPopupChooserBuilder(candidates)
.setRenderer(object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(list: JList<*>?,
value: Any,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean): Component {
val candidate = value as InsertionCandidate
val text = candidate.description()
return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus)
}
})
.setItemSelectedCallback { value: InsertionCandidate? ->
if (value == null) return@setItemSelectedCallback
highlighter.onSelect(value)
}
.setTitle("Choose scope to introduce constant ${expr.text}")
.setMovable(true)
.setResizable(false)
.setRequestFocus(true)
.setItemChosenCallback { it?.let { callback(it) } }
.addListener(highlighter)
.createPopup()
.showInBestPositionFor(editor)
}
}

interface ExtractConstantUi {
fun chooseInsertionPoint(expr: RsExpr, candidates: List<InsertionCandidate>): InsertionCandidate
}

data class InsertionCandidate(val context: PsiElement, val parent: PsiElement, val anchor: PsiElement) {
fun description(): String = when (val element = this.context) {
is RsFunction -> "fn ${element.name}"
is RsModItem -> "mod ${element.name}"
is RsFile -> "file"
else -> error("unreachable")
}
}

private fun findInsertionCandidates(expr: RsExpr): List<InsertionCandidate> {
var parent: PsiElement = expr
var anchor: PsiElement = expr
val points = mutableListOf<InsertionCandidate>()

fun getAnchor(parent: PsiElement, anchor: PsiElement): PsiElement {
var found = anchor
while (found.parent != parent) {
found = found.parent
}
return found
}

var moduleVisited = false
while (parent !is RsFile) {
parent = parent.parent
when (parent) {
is RsFunction -> {
Kobzol marked this conversation as resolved.
Show resolved Hide resolved
if (!moduleVisited) {
parent.block?.let {
points.add(InsertionCandidate(parent, it, getAnchor(it, anchor)))
anchor = parent
}
}
}
is RsModItem, is RsFile -> {
points.add(InsertionCandidate(parent, parent, getAnchor(parent, anchor)))
anchor = parent
moduleVisited = true
}
}
}
return points
}

var MOCK: ExtractConstantUi? = null

@TestOnly
fun withMockExtractConstantChooser(mock: ExtractConstantUi, f: () -> Unit) {
MOCK = mock
try {
f()
} finally {
MOCK = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ class RsIntroduceParameterHandler : RefactoringActionHandler {
}

override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext?) {
//this doesn't get called form the editor.
//this doesn't get called from the editor.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class RsIntroduceVariableHandler : RefactoringActionHandler {
}

override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext?) {
//this doesn't get called form the editor.
//this doesn't get called from the editor.
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/main/kotlin/org/rust/lang/core/psi/RsPsiFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class RsPsiFactory(
val typeText = it.type.renderInsertionSafe(includeLifetimeArguments = true)
"${"pub".iff(it.addPub)}${it.name}: $typeText"
}
return createFromText("struct S { $fieldsText }") ?: error("Failed to create block fields")
return createFromText("struct S { $fieldsText }") ?: error("Failed to create block fields")
}

fun createTupleFields(fields: List<TupleField>): RsTupleFields {
Expand Down Expand Up @@ -328,6 +328,10 @@ class RsPsiFactory(
?: error("Failed to create match body from patterns: `$arms`")
}

fun createConstant(name: String, expr: RsExpr): RsConstant =
createFromText("const $name: ${expr.type.renderInsertionSafe(useAliasNames = true, includeLifetimeArguments = true)} = ${expr.text};")
?: error("Failed to create constant $name from ${expr.text} ")

private inline fun <reified T : RsElement> createFromText(code: CharSequence): T? =
createFile(code).descendantOfTypeStrict()

Expand Down
Loading