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

Extract value refactoring #3251

Merged
merged 31 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
40f3fd0
Extract variable pt 1.
jojo2357 Sep 19, 2023
e500780
Freestyle when no selection
jojo2357 Sep 19, 2023
5d62bbd
Search entire file
jojo2357 Sep 19, 2023
31b6f4d
Clean up some
jojo2357 Sep 19, 2023
561558f
Put the definition in the correct place
jojo2357 Sep 19, 2023
18a5291
Put the definition in the correct place
jojo2357 Sep 20, 2023
d8ed722
Added some tests
jojo2357 Sep 20, 2023
65be3c0
Fix tests
jojo2357 Sep 20, 2023
25974f0
Fix semantic error and failing test
jojo2357 Sep 20, 2023
0fe8675
Add new test for extracting an environment
jojo2357 Sep 20, 2023
6c3e285
Don't run Qodana on draft PRs
PHPirates Sep 21, 2023
3c7913e
Foundation to extract text-index-specific data
jojo2357 Sep 22, 2023
088837b
Highlighting MAGIC
jojo2357 Sep 22, 2023
efbdb3f
Fix Int Range
jojo2357 Sep 22, 2023
f957f16
Improve tests
jojo2357 Sep 22, 2023
80d8f4e
Dont offer something twice
jojo2357 Sep 23, 2023
68cfc51
distinct better
jojo2357 Sep 23, 2023
7b3646e
Add comment to LatexExtractCommandHandler
PHPirates Sep 24, 2023
7b42ab3
Fix test
jojo2357 Sep 24, 2023
a044540
All tests passing
jojo2357 Sep 26, 2023
7749240
Lint
jojo2357 Sep 26, 2023
c68f42d
Fix parameter error
jojo2357 Oct 1, 2023
e7db958
Remove unnecessary `{}`
jojo2357 Oct 1, 2023
a229dde
Pass tests
jojo2357 Oct 1, 2023
dc610ae
Distribute utilities
jojo2357 Oct 1, 2023
434909a
Reduce complexity, increase performance
jojo2357 Oct 1, 2023
caf0ed5
Clean Up
jojo2357 Oct 17, 2023
a4dade0
Reduce code duplication
PHPirates Nov 24, 2023
a7bd6fd
Remove hostility and clean up
PHPirates Nov 24, 2023
a25b6f5
Capitalization
PHPirates Nov 24, 2023
18ea366
Formatting
PHPirates Nov 24, 2023
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
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
name: Qodana
# Note: updating to ubuntu-latest may slow the build and produce a "The runner has received a shutdown signal."
runs-on: ubuntu-20.04
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package nl.hannahsten.texifyidea.refactoring

import com.intellij.lang.refactoring.RefactoringSupportProvider
import com.intellij.psi.PsiElement
import com.intellij.refactoring.RefactoringActionHandler
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.refactoring.myextractfunction.LatexExtractCommandHandler

/**
* This class is used to enable inline refactoring.
Expand All @@ -20,4 +22,6 @@ class LatexRefactoringSupportProvider : RefactoringSupportProvider() {
override fun isSafeDeleteAvailable(element: PsiElement): Boolean {
return element is LatexParameterText
}

override fun getIntroduceVariableHandler(): RefactoringActionHandler = LatexExtractCommandHandler()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package nl.hannahsten.texifyidea.refactoring.myextractfunction

import com.intellij.codeInsight.PsiEquivalenceUtil
import com.intellij.ide.plugins.PluginManagerCore.isUnitTestMode
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.Pass
import com.intellij.psi.*
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiTreeUtil.findCommonParent
import com.intellij.psi.util.elementType
import com.intellij.psi.util.parents
import com.intellij.refactoring.IntroduceTargetChooser
import com.intellij.refactoring.RefactoringActionHandler
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.introduce.inplace.OccurrencesChooser
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import com.intellij.refactoring.util.CommonRefactoringUtil
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.psi.LatexTypes.NORMAL_TEXT_WORD
import nl.hannahsten.texifyidea.util.insertCommandDefinition
import nl.hannahsten.texifyidea.util.parser.*
import nl.hannahsten.texifyidea.util.runWriteCommandAction
import org.jetbrains.annotations.TestOnly

class LatexExtractCommandHandler : RefactoringActionHandler {
override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) {
if (file !is LatexFile) return
val exprs = findCandidateExpressionsToExtract(editor, file)

if (exprs.isEmpty()) {
val message = RefactoringBundle.message(
if (editor.selectionModel.hasSelection())
"selected.block.should.represent.an.expression"
else
"refactoring.introduce.selection.error"
)
val title = RefactoringBundle.message("introduce.variable.title")
val helpId = "refactoring.extractVariable"
CommonRefactoringUtil.showErrorHint(project, editor, message, title, helpId)
}
else {
val extractor = { expr: PsiElement ->
extractExpression(
editor, expr, RefactoringBundle.message("introduce.variable.title")
)
}
if (exprs.size == 1) {
extractor(exprs.single())
}
else showExpressionChooser(editor, exprs) {
extractor(it)
}
}
}

override fun invoke(project: Project, elements: Array<out PsiElement>, dataContext: DataContext?) {
TODO("This was not meant to happen like this")
}
}

fun showExpressionChooser(
editor: Editor,
exprs: List<PsiElement>,
callback: (PsiElement) -> Unit
) {
if (isUnitTestMode) {
callback(MOCK!!.chooseTarget(exprs))
} else
IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass) { it.text }
}

fun extractExpression(
editor: Editor,
expr: PsiElement,
@Suppress("UnstableApiUsage")
@NlsContexts.Command commandName: String
) {
if (!expr.isValid) return
val occurrences = findOccurrences(expr)
showOccurrencesChooser(editor, expr, occurrences) { occurrencesToReplace ->
ExpressionReplacer(expr.project, editor, expr)
.replaceElementForAllExpr(occurrencesToReplace, commandName)
}
}

private class ExpressionReplacer(
private val project: Project,
private val editor: Editor,
private val chosenExpr: PsiElement
) {
private val psiFactory = LatexPsiHelper(project)

fun replaceElementForAllExpr(
exprs: List<PsiElement>,
@Suppress("UnstableApiUsage")
@NlsContexts.Command commandName: String
) {
val sortedExprs = exprs.sortedBy { it.startOffset }
val firstExpr = sortedExprs.firstOrNull() ?: chosenExpr

val name = psiFactory.createFromText("\\mycommand{}").firstChildOfType(LatexCommands::class) ?: return

runWriteCommandAction(project, commandName) {
val letBinding = insertCommandDefinition(chosenExpr.containingFile, chosenExpr.text)
?: return@runWriteCommandAction //firstExpr.parent.addBefore(newcommand, firstExpr)
exprs.filter{ it != chosenExpr }.forEach { it.replace(name) }
val chosenInsertion = chosenExpr.replace(name)

PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
val filterIsInstance =
letBinding.childrenOfType(PsiNamedElement::class).filterIsInstance<LatexCommands>()
val actualToken =
filterIsInstance.firstOrNull { it.text == "\\mycommand" }
?: throw IllegalStateException("How did this happen??")

editor.caretModel.moveToOffset(chosenInsertion.textRange.startOffset)

LatexInPlaceVariableIntroducer(
actualToken, editor, project, "choose a variable"
)
.performInplaceRefactoring(LinkedHashSet())
}
}
}

fun showOccurrencesChooser(
editor: Editor,
expr: PsiElement,
occurrences: List<PsiElement>,
callback: (List<PsiElement>) -> Unit
) {
if (isUnitTestMode && occurrences.size > 1) {
callback(MOCK!!.chooseOccurrences(expr, occurrences))
} else {
OccurrencesChooser.simpleChooser<PsiElement>(editor)
.showChooser(
expr,
occurrences,
{ choice: OccurrencesChooser.ReplaceChoice ->
val toReplace = if (choice == OccurrencesChooser.ReplaceChoice.ALL) occurrences else listOf(expr)
callback(toReplace)
}.asPass
)
}
}

private val <T> ((T) -> Unit).asPass: Pass<T>
get() = object : Pass<T>() {
override fun pass(t: T) = this@asPass(t)
}

fun findExpressionInRange(file: PsiFile, startOffset: Int, endOffset: Int): PsiElement? {
val firstUnresolved = file.findElementAt(startOffset) ?: return null
val first =
if (firstUnresolved is PsiWhiteSpace)
file.findElementAt(firstUnresolved.endOffset) ?: return null
else
firstUnresolved

val lastUnresolved = file.findElementAt(endOffset - 1) ?: return null
val last =
if (lastUnresolved is PsiWhiteSpace)
file.findElementAt(lastUnresolved.startOffset - 1) ?: return null
else
lastUnresolved

return findCommonParent(first, last)
}

fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List<PsiElement> {
val selection = editor.selectionModel
return if (selection.hasSelection()) {
// If there's an explicit selection, suggest only one expression
listOfNotNull(findExpressionInRange(file, selection.selectionStart, selection.selectionEnd))
}
else {
val expr = findExpressionAtCaret(file, editor.caretModel.offset)
?: return emptyList()
if (expr is LatexBeginCommand) {
val endCommand = expr.endCommand()
if (endCommand == null)
emptyList()
else
{
val environToken = findCommonParent(expr, endCommand)
if (environToken != null)
listOf(environToken)
else
emptyList()
}
} else
expr.parents(true)
.takeWhile { it.elementType == NORMAL_TEXT_WORD || it is LatexNormalText || it is LatexParameter || it is LatexMathContent || it is LatexCommandWithParams }
.distinctBy { it.text }
.toList()
}
}


fun findExpressionAtCaret(file: LatexFile, offset: Int): PsiElement? {
val expr = file.expressionAtOffset(offset)
val exprBefore = file.expressionAtOffset(offset - 1)
return when {
expr == null -> exprBefore
exprBefore == null -> expr
PsiTreeUtil.isAncestor(expr, exprBefore, false) -> exprBefore
else -> expr
}
}

/**
* Gets the smallest extractable expression at the given offset
*/
fun LatexFile.expressionAtOffset(offset: Int): PsiElement? {
val element = findElementAt(offset) ?: return null

return element.parents(true)
.firstOrNull { it.elementType == NORMAL_TEXT_WORD || it is LatexNormalText || it is LatexParameter || it is LatexMathContent || it is LatexCommandWithParams }
}

/**
* Finds occurrences in the sub scope of expr, so that all will be replaced if replace all is selected.
*/
fun findOccurrences(expr: PsiElement): List<PsiElement> {
val parent = expr.parentOfType(LatexFile::class)
?: return emptyList()
return findOccurrences(parent, expr)
}

fun findOccurrences(parent: PsiElement, expr: PsiElement): List<PsiElement> {
val visitor = object : PsiRecursiveElementVisitor() {
val foundOccurrences = ArrayList<PsiElement>()
override fun visitElement(element: PsiElement) {
if (PsiEquivalenceUtil.areElementsEquivalent(expr, element)) {
foundOccurrences.add(element)
}
else {
super.visitElement(element)
}
}
}
parent.acceptChildren(visitor)
return visitor.foundOccurrences
}

interface ExtractExpressionUi {
fun chooseTarget(exprs: List<PsiElement>): PsiElement
fun chooseOccurrences(expr: PsiElement, occurrences: List<PsiElement>): List<PsiElement>
}

var MOCK: ExtractExpressionUi? = null
@TestOnly
fun withMockTargetExpressionChooser(mock: ExtractExpressionUi, f: () -> Unit) {
MOCK = mock
try {
f()
} finally {
MOCK = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package nl.hannahsten.texifyidea.refactoring.myextractfunction

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.Pair
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNamedElement
import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer

class LatexInPlaceVariableIntroducer(
elementToRename: PsiNamedElement,
editor: Editor,
project: Project,
@Suppress("UnstableApiUsage") @NlsContexts.Command title: String,
private val additionalElementsToRename: List<PsiElement> = emptyList()
) : InplaceVariableIntroducer<PsiElement>(elementToRename, editor, project, title, emptyArray(), null) {

override fun collectAdditionalElementsToRename(stringUsages: MutableList<in Pair<PsiElement, TextRange>>) {
for (element in additionalElementsToRename) {
if (element.isValid) {
stringUsages.add(Pair(element, TextRange(0, element.textLength)))
}
}
}
}
Loading