From 40f3fd023fbfa1a9ff81aa4aecb2039b84509126 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:21:52 -0600 Subject: [PATCH 01/31] Extract variable pt 1. It sucks. A lot. --- .../LatexRefactoringSupportProvider.kt | 4 + .../LatexExtractCommandHandler.kt | 281 ++++++++++++++++++ .../LatexInPlaceVariableIntroducer.kt | 27 ++ src/nl/hannahsten/texifyidea/util/General.kt | 14 +- 4 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt create mode 100644 src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexInPlaceVariableIntroducer.kt diff --git a/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt b/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt index 14084c070..4ee14714d 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt @@ -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. @@ -20,4 +22,6 @@ class LatexRefactoringSupportProvider : RefactoringSupportProvider() { override fun isSafeDeleteAvailable(element: PsiElement): Boolean { return element is LatexParameterText } + + override fun getIntroduceVariableHandler(): RefactoringActionHandler = LatexExtractCommandHandler() } \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt new file mode 100644 index 000000000..a459e1c5e --- /dev/null +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -0,0 +1,281 @@ +package nl.hannahsten.texifyidea.refactoring.myextractfunction + +import com.intellij.codeInsight.PsiEquivalenceUtil +import com.intellij.openapi.actionSystem.DataContext +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.parents +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.util.parser.childrenOfType +import nl.hannahsten.texifyidea.util.parser.firstChildOfType +import nl.hannahsten.texifyidea.util.runWriteCommandAction + +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, ":)"//RsBundle.message("command.name.introduce.local.variable") + ) + } + if (exprs.size == 1) { + extractor(exprs.single()) + } + else TODO(":(") + + /*else showExpressionChooser(editor, exprs) { + extractor(it) + }*/ + } + /* + val start = editor.selectionModel.selectionStart + val end = editor.selectionModel.selectionEnd + if (start === null || end === null) return + + // what do we need to do? + */ + /* + Resolve the current selection. If this is text, we need to ask to extract the current word, sentence, paragraph. + If this is inter-environmental, we need to select all the environments. + *//* + + val firstUnresolved = file.findElementAt(start) ?: return + val first = + if (firstUnresolved is PsiWhiteSpace) + file.findElementAt(firstUnresolved.startOffset - 1) ?: return + else + firstUnresolved + + val lastUnresolved = file.findElementAt(end - 1) ?: return + val last = + if (lastUnresolved is PsiWhiteSpace) + file.findElementAt(lastUnresolved.endOffset) ?: return + else + lastUnresolved + + val parent = findCommonParent(first, last) + + // should be doing extra here? + + val psiSeq = generateSequence(first) { + if (it.nextSibling == last) + null + else + it.nextSibling + } + + val entries = PsiUtilCore.toPsiElementArray(psiSeq.filter{ it !is PsiWhiteSpace}.toList()) + + if (entries.isEmpty()) return + + println("I would have extracted " + entries.fold ("") { out, curr -> out + curr.text + " " }); +*/ + + /* + We need to find other usages of this, so we can replace them too + */ + + /* + We need to create the command calls + */ + + /* + we need to settle on a name for those calls + */ + } + + override fun invoke(project: Project, elements: Array, dataContext: DataContext?) { + TODO("This was not meant to happen like this") + } +} + +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, + @Suppress("UnstableApiUsage") + @NlsContexts.Command commandName: String + ) { + val sortedExprs = exprs.sortedBy { it.startOffset } + val firstExpr = sortedExprs.firstOrNull() ?: chosenExpr + + val newcommand = psiFactory.createFromText("\\newcommand{\\mycommand}{${chosenExpr.text}}").firstChild + val name = psiFactory.createFromText("\\mycommand{}").firstChildOfType(LatexCommands::class) ?: return + + runWriteCommandAction(project, commandName) { + val letBinding = firstExpr.parent.addBefore(newcommand, firstExpr) + exprs.forEach { it.replace(name) } + + + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) + //name.commandToken + val actualToken = + letBinding.childrenOfType(PsiNameIdentifierOwner::class).filterIsInstance() + .first { it.text == "\\mycommand" } + + editor.caretModel.moveToOffset(actualToken.textRange.startOffset) + + LatexInPlaceVariableIntroducer( + actualToken, editor, project, "Choose me!" + ) + .performInplaceRefactoring(LinkedHashSet()) + } + } +} + +fun showOccurrencesChooser( + editor: Editor, + expr: PsiElement, + occurrences: List, + callback: (List) -> Unit +) { + OccurrencesChooser.simpleChooser(editor) + .showChooser( + expr, + occurrences, + { choice: OccurrencesChooser.ReplaceChoice -> + val toReplace = if (choice == OccurrencesChooser.ReplaceChoice.ALL) occurrences else listOf(expr) + callback(toReplace) + }.asPass + ) +} + +private val ((T) -> Unit).asPass: Pass + get() = object : Pass() { + 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.startOffset - 1) ?: return null + else + firstUnresolved + + val lastUnresolved = file.findElementAt(endOffset - 1) ?: return null + val last = + if (lastUnresolved is PsiWhiteSpace) + file.findElementAt(lastUnresolved.endOffset) ?: return null + else + lastUnresolved + + return findCommonParent(first, last) +} + +fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { + 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() + // Finds possible expressions that might want to be bound to a local variable. + // We don't go further than the current block scope, + // further more path expressions don't make sense to bind to a local variable so we exclude them. + expr.parents(true) + .takeWhile { it is LatexNormalText || it is LatexParameter || it is LatexMathContent || it is LatexCommandWithParams } + .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 + } +} + +fun LatexFile.expressionAtOffset(offset: Int): PsiElement? { + val element = findElementAt(offset) ?: return null + + return element.parents(true) + .firstOrNull { 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 { + val parent = expr.parent + ?: return emptyList() + return findOccurrences(parent, expr) +} + +fun findOccurrences(parent: PsiElement, expr: PsiElement): List { + val visitor = object : PsiRecursiveElementVisitor() { + val foundOccurrences = ArrayList() + override fun visitElement(element: PsiElement) { + if (PsiEquivalenceUtil.areElementsEquivalent(expr, element)) { + foundOccurrences.add(element) + } + else { + super.visitElement(element) + } + } + } + parent.acceptChildren(visitor) + return visitor.foundOccurrences +} + +/* +fun moveEditorToNameElement(editor: Editor, element: PsiElement?): RsPatBinding? { + val newName = element?.findBinding() + editor.caretModel.moveToOffset(newName?.identifier?.textRange?.startOffset ?: 0) + return newName +} + +fun PsiElement.findBinding() = PsiTreeUtil.findChildOfType(this,::class.java) +*/ diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexInPlaceVariableIntroducer.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexInPlaceVariableIntroducer.kt new file mode 100644 index 000000000..90a788fbf --- /dev/null +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexInPlaceVariableIntroducer.kt @@ -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 = emptyList() +) : InplaceVariableIntroducer(elementToRename, editor, project, title, emptyArray(), null) { + + override fun collectAdditionalElementsToRename(stringUsages: MutableList>) { + for (element in additionalElementsToRename) { + if (element.isValid) { + stringUsages.add(Pair(element, TextRange(0, element.textLength))) + } + } + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/util/General.kt b/src/nl/hannahsten/texifyidea/util/General.kt index f27a28d15..58ad29de9 100644 --- a/src/nl/hannahsten/texifyidea/util/General.kt +++ b/src/nl/hannahsten/texifyidea/util/General.kt @@ -4,6 +4,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiFile import java.util.regex.Pattern /** @@ -32,6 +33,16 @@ fun runWriteCommandAction(project: Project, writeCommandAction: () -> Unit) { WriteCommandAction.runWriteCommandAction(project, writeCommandAction) } +fun runWriteCommandAction( + project: Project, + commandName: String, + vararg files: PsiFile, + writeCommandAction: () -> T +): T { + return WriteCommandAction.writeCommandAction(project, *files).withName(commandName) + .compute(writeCommandAction) +} + /** * Converts an [IntRange] to [TextRange]. */ @@ -47,7 +58,8 @@ val IntRange.length: Int * Converts the range to a range representation with the given seperator. * When the range has size 0, it will only print the single number. */ -fun IntRange.toRangeString(separator: String = "-") = if (start == endInclusive) start else "$start$separator$endInclusive" +fun IntRange.toRangeString(separator: String = "-") = + if (start == endInclusive) start else "$start$separator$endInclusive" /** * Shift the range to the right by the number of places given. From e500780755539c542235af5dcbce178ca31b2aa5 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Mon, 18 Sep 2023 23:08:06 -0600 Subject: [PATCH 02/31] Freestyle when no selection --- .../LatexExtractCommandHandler.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt index a459e1c5e..737b436b9 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -9,7 +9,9 @@ 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 @@ -18,6 +20,7 @@ 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.parser.childrenOfType import nl.hannahsten.texifyidea.util.parser.firstChildOfType import nl.hannahsten.texifyidea.util.runWriteCommandAction @@ -46,8 +49,9 @@ class LatexExtractCommandHandler : RefactoringActionHandler { } if (exprs.size == 1) { extractor(exprs.single()) + } else showExpressionChooser(editor, exprs) { + extractor(it) } - else TODO(":(") /*else showExpressionChooser(editor, exprs) { extractor(it) @@ -115,6 +119,14 @@ class LatexExtractCommandHandler : RefactoringActionHandler { } } +fun showExpressionChooser( + editor: Editor, + exprs: List, + callback: (PsiElement) -> Unit +) { + IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass) { it.text } +} + fun extractExpression( editor: Editor, expr: PsiElement, @@ -153,7 +165,6 @@ private class ExpressionReplacer( PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) - //name.commandToken val actualToken = letBinding.childrenOfType(PsiNameIdentifierOwner::class).filterIsInstance() .first { it.text == "\\mycommand" } @@ -221,7 +232,7 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List Date: Tue, 19 Sep 2023 00:24:27 -0600 Subject: [PATCH 03/31] Search entire file --- .../myextractfunction/LatexExtractCommandHandler.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt index 737b436b9..8f8b84300 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -10,6 +10,7 @@ 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.findTopmostParentInFile import com.intellij.psi.util.parents import com.intellij.refactoring.IntroduceTargetChooser import com.intellij.refactoring.RefactoringActionHandler @@ -23,6 +24,7 @@ import nl.hannahsten.texifyidea.psi.* import nl.hannahsten.texifyidea.psi.LatexTypes.NORMAL_TEXT_WORD import nl.hannahsten.texifyidea.util.parser.childrenOfType import nl.hannahsten.texifyidea.util.parser.firstChildOfType +import nl.hannahsten.texifyidea.util.parser.parentOfType import nl.hannahsten.texifyidea.util.runWriteCommandAction class LatexExtractCommandHandler : RefactoringActionHandler { @@ -260,7 +262,7 @@ fun LatexFile.expressionAtOffset(offset: Int): PsiElement? { */ fun findOccurrences(expr: PsiElement): List { - val parent = expr.parent + val parent = expr.parentOfType(LatexFile::class) ?: return emptyList() return findOccurrences(parent, expr) } From 31b6f4dce1e7a3f2fbf461d305573f4b1df20384 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Tue, 19 Sep 2023 00:56:43 -0600 Subject: [PATCH 04/31] Clean up some --- .../LatexExtractCommandHandler.kt | 80 +------------------ 1 file changed, 4 insertions(+), 76 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt index 8f8b84300..265185211 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -10,7 +10,6 @@ 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.findTopmostParentInFile import com.intellij.psi.util.parents import com.intellij.refactoring.IntroduceTargetChooser import com.intellij.refactoring.RefactoringActionHandler @@ -46,74 +45,16 @@ class LatexExtractCommandHandler : RefactoringActionHandler { else { val extractor = { expr: PsiElement -> extractExpression( - editor, expr, ":)"//RsBundle.message("command.name.introduce.local.variable") + editor, expr, RefactoringBundle.message("introduce.variable.title") ) } if (exprs.size == 1) { extractor(exprs.single()) - } else showExpressionChooser(editor, exprs) { - extractor(it) } - - /*else showExpressionChooser(editor, exprs) { + else showExpressionChooser(editor, exprs) { extractor(it) - }*/ - } - /* - val start = editor.selectionModel.selectionStart - val end = editor.selectionModel.selectionEnd - if (start === null || end === null) return - - // what do we need to do? - */ - /* - Resolve the current selection. If this is text, we need to ask to extract the current word, sentence, paragraph. - If this is inter-environmental, we need to select all the environments. - *//* - - val firstUnresolved = file.findElementAt(start) ?: return - val first = - if (firstUnresolved is PsiWhiteSpace) - file.findElementAt(firstUnresolved.startOffset - 1) ?: return - else - firstUnresolved - - val lastUnresolved = file.findElementAt(end - 1) ?: return - val last = - if (lastUnresolved is PsiWhiteSpace) - file.findElementAt(lastUnresolved.endOffset) ?: return - else - lastUnresolved - - val parent = findCommonParent(first, last) - - // should be doing extra here? - - val psiSeq = generateSequence(first) { - if (it.nextSibling == last) - null - else - it.nextSibling + } } - - val entries = PsiUtilCore.toPsiElementArray(psiSeq.filter{ it !is PsiWhiteSpace}.toList()) - - if (entries.isEmpty()) return - - println("I would have extracted " + entries.fold ("") { out, curr -> out + curr.text + " " }); -*/ - - /* - We need to find other usages of this, so we can replace them too - */ - - /* - We need to create the command calls - */ - - /* - we need to settle on a name for those calls - */ } override fun invoke(project: Project, elements: Array, dataContext: DataContext?) { @@ -174,7 +115,7 @@ private class ExpressionReplacer( editor.caretModel.moveToOffset(actualToken.textRange.startOffset) LatexInPlaceVariableIntroducer( - actualToken, editor, project, "Choose me!" + actualToken, editor, project, "choose a variable" ) .performInplaceRefactoring(LinkedHashSet()) } @@ -230,9 +171,6 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { parent.acceptChildren(visitor) return visitor.foundOccurrences } - -/* -fun moveEditorToNameElement(editor: Editor, element: PsiElement?): RsPatBinding? { - val newName = element?.findBinding() - editor.caretModel.moveToOffset(newName?.identifier?.textRange?.startOffset ?: 0) - return newName -} - -fun PsiElement.findBinding() = PsiTreeUtil.findChildOfType(this,::class.java) -*/ From 561558f9e35431c1802926a12365aae681611754 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:11:31 -0600 Subject: [PATCH 05/31] Put the definition in the correct place --- .../LatexExtractCommandHandler.kt | 15 ++- src/nl/hannahsten/texifyidea/util/Commands.kt | 92 +++++++++++++++++++ 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt index 265185211..55806950e 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -21,10 +21,13 @@ 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.files.findRootFile +import nl.hannahsten.texifyidea.util.insertCommandDefinition import nl.hannahsten.texifyidea.util.parser.childrenOfType import nl.hannahsten.texifyidea.util.parser.firstChildOfType import nl.hannahsten.texifyidea.util.parser.parentOfType import nl.hannahsten.texifyidea.util.runWriteCommandAction +import java.lang.IllegalStateException class LatexExtractCommandHandler : RefactoringActionHandler { override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) { @@ -99,18 +102,19 @@ private class ExpressionReplacer( val sortedExprs = exprs.sortedBy { it.startOffset } val firstExpr = sortedExprs.firstOrNull() ?: chosenExpr - val newcommand = psiFactory.createFromText("\\newcommand{\\mycommand}{${chosenExpr.text}}").firstChild +// val newcommand = psiFactory.createFromText("\\newcommand{\\mycommand}{${chosenExpr.text}}").firstChildOfType(PsiNameIdentifierOwner::class) ?: throw IllegalStateException("This isnt doable") val name = psiFactory.createFromText("\\mycommand{}").firstChildOfType(LatexCommands::class) ?: return runWriteCommandAction(project, commandName) { - val letBinding = firstExpr.parent.addBefore(newcommand, firstExpr) + val letBinding = insertCommandDefinition(chosenExpr.containingFile, chosenExpr.text) ?: return@runWriteCommandAction //firstExpr.parent.addBefore(newcommand, firstExpr) exprs.forEach { it.replace(name) } - PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) - val actualToken = + val filterIsInstance = letBinding.childrenOfType(PsiNameIdentifierOwner::class).filterIsInstance() - .first { it.text == "\\mycommand" } + val actualToken = + filterIsInstance.firstOrNull { it.text == "\\mycommand" } ?: + throw IllegalStateException("How did this happen??") editor.caretModel.moveToOffset(actualToken.textRange.startOffset) @@ -173,6 +177,7 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { .toSet() } +/** + * Inserts a usepackage statement for the given package in a certain file. + * + * @param file + * The file to add the usepackage statement to. + * @param packageName + * The name of the package to insert. + * @param parameters + * Parameters to add to the statement, `null` or empty string for no parameters. + */ +fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: String = "mycommand"): PsiElement? { + if (!file.isWritable) return null + + val commands = file.commandsInFile() + + val definitionCommandName = if (file.isStyleFile() || file.isClassFile()) "\\RequirePackage" else "\\usepackage" + + var last: LatexCommands? = null + for (cmd in commands) { + if (cmd.commandToken.text == "\\newcommand") { + last = cmd + } else if (cmd.commandToken.text == "\\usepackage") { + last = cmd + } else if (cmd.commandToken.text == "\\begin" && cmd.requiredParameter(0) == "document") { + last = cmd + break + } + } + + // The anchor after which the new element will be inserted + val anchorAfter: PsiElement? + + // When there are no usepackage commands: insert below documentclass. + if (last == null) { + val classHuh = commands.asSequence() + .filter { cmd -> + "\\documentclass" == cmd.commandToken + .text || "\\LoadClass" == cmd.commandToken.text + } + .firstOrNull() + if (classHuh != null) { + anchorAfter = classHuh + } + else { + // No other sensible location can be found + anchorAfter = null + } + } + // Otherwise, insert below the lowest usepackage. + else { + anchorAfter = last + } + + val blockingNames = file.definitions().filter { it.commandToken.text.matches("${newCommandName}\\d*".toRegex()) } + + val nonConflictingName = "${newCommandName}${if (blockingNames.isEmpty()) "" else blockingNames.size.toString()}" + val command = "\\newcommand{\\$nonConflictingName}{${commandText}}"; + + val newNode = LatexPsiHelper(file.project).createFromText(command).firstChild.node + + // Don't run in a write action, as that will produce a SideEffectsNotAllowedException for INVOKE_LATER + + // Avoid "Attempt to modify PSI for non-committed Document" + // https://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/modifying_psi.html?search=refac#combining-psi-and-document-modifications + PsiDocumentManager.getInstance(file.project) + .doPostponedOperationsAndUnblockDocument(file.document() ?: return null) + PsiDocumentManager.getInstance(file.project).commitDocument(file.document() ?: return null) + + runWriteAction { + // Avoid NPE, see #3083 (cause unknown) + if (anchorAfter != null && com.intellij.psi.impl.source.tree.TreeUtil.getFileElement(anchorAfter.parent.node) != null) { + val anchorBefore = anchorAfter.node.treeNext + val newLine = LatexPsiHelper(file.project).createFromText("\n\n").firstChild.node + anchorAfter.parent.node.addChild(newLine, anchorBefore) + anchorAfter.parent.node.addChild(newNode, anchorBefore) + } + else { + // Insert at beginning + file.node.addChild(newNode, file.firstChild.node) + } + } + + return newNode.psi +} + /** * Expand custom commands in a given text once, using its definition in the [LatexCommandsIndex]. */ From 18a52911b4affda9337f1a91dd9e8df4daa4d2a5 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Tue, 19 Sep 2023 21:03:32 -0600 Subject: [PATCH 06/31] Put the definition in the correct place --- .../LatexExtractCommandHandler.kt | 22 ++++++++++--------- src/nl/hannahsten/texifyidea/util/Commands.kt | 2 -- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt index 55806950e..1f45d8e8d 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -21,13 +21,11 @@ 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.files.findRootFile import nl.hannahsten.texifyidea.util.insertCommandDefinition import nl.hannahsten.texifyidea.util.parser.childrenOfType import nl.hannahsten.texifyidea.util.parser.firstChildOfType import nl.hannahsten.texifyidea.util.parser.parentOfType import nl.hannahsten.texifyidea.util.runWriteCommandAction -import java.lang.IllegalStateException class LatexExtractCommandHandler : RefactoringActionHandler { override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) { @@ -102,21 +100,22 @@ private class ExpressionReplacer( val sortedExprs = exprs.sortedBy { it.startOffset } val firstExpr = sortedExprs.firstOrNull() ?: chosenExpr -// val newcommand = psiFactory.createFromText("\\newcommand{\\mycommand}{${chosenExpr.text}}").firstChildOfType(PsiNameIdentifierOwner::class) ?: throw IllegalStateException("This isnt doable") 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.forEach { it.replace(name) } + 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(PsiNameIdentifierOwner::class).filterIsInstance() + letBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() val actualToken = - filterIsInstance.firstOrNull { it.text == "\\mycommand" } ?: - throw IllegalStateException("How did this happen??") + filterIsInstance.firstOrNull { it.text == "\\mycommand" } + ?: throw IllegalStateException("How did this happen??") - editor.caretModel.moveToOffset(actualToken.textRange.startOffset) + editor.caretModel.moveToOffset(chosenInsertion.textRange.startOffset) LatexInPlaceVariableIntroducer( actualToken, editor, project, "choose a variable" @@ -182,6 +181,7 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { val parent = expr.parentOfType(LatexFile::class) ?: return emptyList() diff --git a/src/nl/hannahsten/texifyidea/util/Commands.kt b/src/nl/hannahsten/texifyidea/util/Commands.kt index bb68fe165..383c6229b 100644 --- a/src/nl/hannahsten/texifyidea/util/Commands.kt +++ b/src/nl/hannahsten/texifyidea/util/Commands.kt @@ -60,8 +60,6 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: val commands = file.commandsInFile() - val definitionCommandName = if (file.isStyleFile() || file.isClassFile()) "\\RequirePackage" else "\\usepackage" - var last: LatexCommands? = null for (cmd in commands) { if (cmd.commandToken.text == "\\newcommand") { From d8ed72284a2d188b6fae005727668006fea249c1 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:09:01 -0600 Subject: [PATCH 07/31] Added some tests --- .../LatexExtractCommandHandler.kt | 46 ++++-- .../refactoring/IntroduceVariableTest.kt | 150 ++++++++++++++++++ 2 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt index 1f45d8e8d..67fbdfb30 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -1,7 +1,9 @@ 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 @@ -26,6 +28,7 @@ import nl.hannahsten.texifyidea.util.parser.childrenOfType import nl.hannahsten.texifyidea.util.parser.firstChildOfType import nl.hannahsten.texifyidea.util.parser.parentOfType 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?) { @@ -68,7 +71,10 @@ fun showExpressionChooser( exprs: List, callback: (PsiElement) -> Unit ) { - IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass) { it.text } + if (isUnitTestMode) { + callback(MOCK!!.chooseTarget(exprs)) + } else + IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass) { it.text } } fun extractExpression( @@ -131,15 +137,19 @@ fun showOccurrencesChooser( occurrences: List, callback: (List) -> Unit ) { - OccurrencesChooser.simpleChooser(editor) - .showChooser( - expr, - occurrences, - { choice: OccurrencesChooser.ReplaceChoice -> - val toReplace = if (choice == OccurrencesChooser.ReplaceChoice.ALL) occurrences else listOf(expr) - callback(toReplace) - }.asPass - ) + if (isUnitTestMode && occurrences.size > 1) { + callback(MOCK!!.chooseOccurrences(expr, occurrences)) + } else { + OccurrencesChooser.simpleChooser(editor) + .showChooser( + expr, + occurrences, + { choice: OccurrencesChooser.ReplaceChoice -> + val toReplace = if (choice == OccurrencesChooser.ReplaceChoice.ALL) occurrences else listOf(expr) + callback(toReplace) + }.asPass + ) + } } private val ((T) -> Unit).asPass: Pass @@ -227,3 +237,19 @@ fun findOccurrences(parent: PsiElement, expr: PsiElement): List { parent.acceptChildren(visitor) return visitor.foundOccurrences } + +interface ExtractExpressionUi { + fun chooseTarget(exprs: List): PsiElement + fun chooseOccurrences(expr: PsiElement, occurrences: List): List +} + +var MOCK: ExtractExpressionUi? = null +@TestOnly +fun withMockTargetExpressionChooser(mock: ExtractExpressionUi, f: () -> Unit) { + MOCK = mock + try { + f() + } finally { + MOCK = null + } +} diff --git a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt new file mode 100644 index 000000000..a810c704f --- /dev/null +++ b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt @@ -0,0 +1,150 @@ +package nl.hannahsten.texifyidea.refactoring + +import com.intellij.psi.PsiElement +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import nl.hannahsten.texifyidea.file.LatexFileType +import nl.hannahsten.texifyidea.refactoring.myextractfunction.ExtractExpressionUi +import nl.hannahsten.texifyidea.refactoring.myextractfunction.withMockTargetExpressionChooser + +class IntroduceVariableTest : BasePlatformTestCase() { + fun testBasicCaret() = doTest( + """ + My favorite number is 5.25 + """, listOf("5.25", "My favorite number is 5.25"), 0, """ + \newcommand{\mycommand}{5.25}My favorite number is \mycommand{} + """ + ) + + fun testBasicSelection() = doTest( + """ + My favorite number is 5.25 + """, emptyList(), 0, """ + \newcommand{\mycommand}{5.25}My favorite number is \mycommand{} + """ + ) + + fun testSentenceOffer() = doTest( + """ + \documentclass[11pt]{article} + + \begin{document} + + \chapter{The significance of 2.718, or e} + + 2.718 is a special number. + + ${'$'}\lim_{x \to \infty} (1+\frac{1}{x})^{x}=2.718${'$'} + + Some old guy discovered 2.718 and was amazed! + + Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} + + \end{document} + """, listOf("2.718", "Some old guy discovered 2.718 and was amazed!", + """Some old guy discovered 2.718 and was amazed! + + Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'}""" + ), 0, """ + \documentclass[11pt]{article} + + \newcommand{\mycommand}{2.718} + + \begin{document} + + \chapter{The significance of \mycommand{}, or e} + + \mycommand{} is a special number. + + ${'$'}\lim_{x \to \infty} (1+\frac{1}{x})^{x}=\mycommand{}${'$'} + + Some old guy discovered \mycommand{} and was amazed! + + Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} + + \end{document} + """ + ) + + fun testMultiTableSelection() = doTest( + """ + \documentclass[11pt]{article} + \begin{document} + + ${'$'}5.25 * 2.68291 = 450${'$'} + + \begin{table}[ht!] + \begin{tabular}{| r |} + \hline + 2.68291 \\ + \hline + \end{tabular} + \end{table} + + \begin{table}[ht!] + \begin{tabular}{| r |} + \hline + 2.68291 \\ + \hline + \end{tabular} + \end{table} + + Some may wonder why 2.68291 is so special. + \end{document} + """, emptyList(), 0, """ + \documentclass[11pt]{article} + + \newcommand{\mycommand}{} + + \begin{document} + + ${'$'}5.25 * \mycommand{} = 450${'$'} + + \begin{table}[ht!] + \begin{tabular}{| r |} + \hline + \mycommand{} \\ + \hline + \end{tabular} + \end{table} + + \begin{table}[ht!] + \begin{tabular}{| r |} + \hline + \mycommand{} \\ + \hline + \end{tabular} + \end{table} + + Some may wonder why \mycommand{} is so special. + \end{document} + """ + ) + + private fun doTest( + before: String, + expressions: List, + target: Int, + after: String, + replaceAll: Boolean = false, + ) { + var shownTargetChooser = false + withMockTargetExpressionChooser(object : ExtractExpressionUi { + override fun chooseTarget(exprs: List): PsiElement { + shownTargetChooser = true + assertEquals(exprs.map { it.text }, expressions) + return exprs[target] + } + + override fun chooseOccurrences(expr: PsiElement, occurrences: List): List = + if (replaceAll) occurrences else listOf(expr) + }) { + myFixture.configureByText(LatexFileType, before.trimIndent()) + myFixture.performEditorAction("IntroduceVariable") + myFixture.checkResult(after.trimIndent()) + + check(expressions.isEmpty() || shownTargetChooser) { + "Chooser isn't shown" + } + } + } +} \ No newline at end of file From 65be3c0d077f697b8c9193e1a02a4c7f72dd42a1 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:11:57 -0600 Subject: [PATCH 08/31] Fix tests --- .../texifyidea/refactoring/IntroduceVariableTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt index a810c704f..7dcea6e46 100644 --- a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt +++ b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt @@ -35,7 +35,7 @@ class IntroduceVariableTest : BasePlatformTestCase() { ${'$'}\lim_{x \to \infty} (1+\frac{1}{x})^{x}=2.718${'$'} - Some old guy discovered 2.718 and was amazed! + Some old guy discovered 2.718 and was amazed! Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} @@ -62,7 +62,7 @@ class IntroduceVariableTest : BasePlatformTestCase() { Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} \end{document} - """ + """, true ) fun testMultiTableSelection() = doTest( @@ -117,7 +117,7 @@ class IntroduceVariableTest : BasePlatformTestCase() { Some may wonder why \mycommand{} is so special. \end{document} - """ + """, true ) private fun doTest( From 25974f0feaec0d6f9d48b18289fe81d413eac2e0 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Wed, 20 Sep 2023 10:49:26 -0600 Subject: [PATCH 09/31] Fix semantic error and failing test --- .../myextractfunction/LatexExtractCommandHandler.kt | 4 ++-- src/nl/hannahsten/texifyidea/util/Commands.kt | 5 +++-- .../texifyidea/refactoring/IntroduceVariableTest.kt | 12 ++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt index 67fbdfb30..714e0fdc8 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -161,14 +161,14 @@ fun findExpressionInRange(file: PsiFile, startOffset: Int, endOffset: Int): PsiE val firstUnresolved = file.findElementAt(startOffset) ?: return null val first = if (firstUnresolved is PsiWhiteSpace) - file.findElementAt(firstUnresolved.startOffset - 1) ?: return null + 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.endOffset) ?: return null + file.findElementAt(lastUnresolved.startOffset - 1) ?: return null else lastUnresolved diff --git a/src/nl/hannahsten/texifyidea/util/Commands.kt b/src/nl/hannahsten/texifyidea/util/Commands.kt index 383c6229b..b14af2d21 100644 --- a/src/nl/hannahsten/texifyidea/util/Commands.kt +++ b/src/nl/hannahsten/texifyidea/util/Commands.kt @@ -101,7 +101,8 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: val nonConflictingName = "${newCommandName}${if (blockingNames.isEmpty()) "" else blockingNames.size.toString()}" val command = "\\newcommand{\\$nonConflictingName}{${commandText}}"; - val newNode = LatexPsiHelper(file.project).createFromText(command).firstChild.node + val newChild = LatexPsiHelper(file.project).createFromText(command).firstChild + val newNode = newChild.node // Don't run in a write action, as that will produce a SideEffectsNotAllowedException for INVOKE_LATER @@ -125,7 +126,7 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: } } - return newNode.psi + return newChild } /** diff --git a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt index 7dcea6e46..275690712 100644 --- a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt +++ b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt @@ -120,6 +120,18 @@ class IntroduceVariableTest : BasePlatformTestCase() { """, true ) + fun testWithQuotes() = doTest( + """ + We could not find strategies that would be of great assistance in this category. + However, if you ever find yourself reading ``Test With Quotes'' I thinnk you for your service. + """, emptyList(), 0, """ + \newcommand{\mycommand}{Test With Quotes} + + We could not find strategies that would be of great assistance in this category. + However, if you ever find yourself reading ``\mycommand{}'' I thinnk you for your service. + """ + ) + private fun doTest( before: String, expressions: List, From 0fe86756360d9f808925a30db01e4c1e5e397e8b Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:20:52 -0600 Subject: [PATCH 10/31] Add new test for extracting an environment --- .../LatexExtractCommandHandler.kt | 25 +++++++++++++------ .../refactoring/IntroduceVariableTest.kt | 22 ++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt index 714e0fdc8..c5d60a0e3 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt @@ -24,9 +24,7 @@ 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.childrenOfType -import nl.hannahsten.texifyidea.util.parser.firstChildOfType -import nl.hannahsten.texifyidea.util.parser.parentOfType +import nl.hannahsten.texifyidea.util.parser.* import nl.hannahsten.texifyidea.util.runWriteCommandAction import org.jetbrains.annotations.TestOnly @@ -184,10 +182,23 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): Listin{enumerate} + \item{Page Data: page id, namespace, title (File Schema: enwiki-latest-page.sql.gz)} + \item{Link Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-pagelinks.sql.gz)} + \item{Redirect Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-redirect.sql.gz)} + \end{enumerate} + """, emptyList(), 0, """ + \newcommand{\mycommand}{\begin{enumerate} + \item{Page Data: page id, namespace, title (File Schema: enwiki-latest-page.sql.gz)} + \item{Link Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-pagelinks.sql.gz)} + \item{Redirect Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-redirect.sql.gz)} + \end{enumerate}} + + Hello Werld + + \mycommand{} + """ + ) + private fun doTest( before: String, expressions: List, From 6c3e285841bbf6bcc5667d30f36af9966ac8adc0 Mon Sep 17 00:00:00 2001 From: Thomas Schouten Date: Thu, 21 Sep 2023 08:51:16 +0200 Subject: [PATCH 11/31] Don't run Qodana on draft PRs --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 746a96e5c..70b7f009b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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: From 3c7913e6f312c2d1988884f6f52bad5a5a66f0d8 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:42:04 -0600 Subject: [PATCH 12/31] Foundation to extract text-index-specific data --- .../LatexRefactoringSupportProvider.kt | 2 +- .../LatexExtractCommandHandler.kt | 79 ++++++++++++------- .../introduceCommand/LatexExtractablePSI.kt | 12 +++ .../LatexInPlaceVariableIntroducer.kt | 2 +- .../refactoring/IntroduceVariableTest.kt | 9 ++- 5 files changed, 71 insertions(+), 33 deletions(-) rename src/nl/hannahsten/texifyidea/refactoring/{myextractfunction => introduceCommand}/LatexExtractCommandHandler.kt (75%) create mode 100644 src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt rename src/nl/hannahsten/texifyidea/refactoring/{myextractfunction => introduceCommand}/LatexInPlaceVariableIntroducer.kt (94%) diff --git a/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt b/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt index 4ee14714d..673b9938c 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt @@ -4,7 +4,7 @@ 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 +import nl.hannahsten.texifyidea.refactoring.introduceCommand.LatexExtractCommandHandler /** * This class is used to enable inline refactoring. diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt similarity index 75% rename from src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt rename to src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index c5d60a0e3..365515583 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -1,17 +1,18 @@ -package nl.hannahsten.texifyidea.refactoring.myextractfunction +package nl.hannahsten.texifyidea.refactoring.introduceCommand 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.openapi.util.TextRange 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.parentOfType import com.intellij.psi.util.parents import com.intellij.refactoring.IntroduceTargetChooser import com.intellij.refactoring.RefactoringActionHandler @@ -45,7 +46,7 @@ class LatexExtractCommandHandler : RefactoringActionHandler { CommonRefactoringUtil.showErrorHint(project, editor, message, title, helpId) } else { - val extractor = { expr: PsiElement -> + val extractor = { expr: LatexExtractablePSI -> extractExpression( editor, expr, RefactoringBundle.message("introduce.variable.title") ) @@ -66,18 +67,18 @@ class LatexExtractCommandHandler : RefactoringActionHandler { fun showExpressionChooser( editor: Editor, - exprs: List, - callback: (PsiElement) -> Unit + exprs: List, + callback: (LatexExtractablePSI) -> Unit ) { if (isUnitTestMode) { callback(MOCK!!.chooseTarget(exprs)) } else - IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass) { it.text } + IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass) { it.text.substring(it.extractableRange.startOffset, it.extractableRange.endOffset) } } fun extractExpression( editor: Editor, - expr: PsiElement, + expr: LatexExtractablePSI, @Suppress("UnstableApiUsage") @NlsContexts.Command commandName: String ) { @@ -131,9 +132,9 @@ private class ExpressionReplacer( fun showOccurrencesChooser( editor: Editor, - expr: PsiElement, - occurrences: List, - callback: (List) -> Unit + expr: LatexExtractablePSI, + occurrences: List, + callback: (List) -> Unit ) { if (isUnitTestMode && occurrences.size > 1) { callback(MOCK!!.chooseOccurrences(expr, occurrences)) @@ -155,7 +156,7 @@ private val ((T) -> Unit).asPass: Pass override fun pass(t: T) = this@asPass(t) } -fun findExpressionInRange(file: PsiFile, startOffset: Int, endOffset: Int): PsiElement? { +fun findExpressionInRange(file: PsiFile, startOffset: Int, endOffset: Int): LatexExtractablePSI? { val firstUnresolved = file.findElementAt(startOffset) ?: return null val first = if (firstUnresolved is PsiWhiteSpace) @@ -170,14 +171,19 @@ fun findExpressionInRange(file: PsiFile, startOffset: Int, endOffset: Int): PsiE else lastUnresolved - return findCommonParent(first, last) + val parent = findCommonParent(first, last) ?: return null + + return if (parent is LatexNormalText) { + LatexExtractablePSI(parent, TextRange(startOffset, endOffset)) + } else + LatexExtractablePSI(parent) } -fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { +fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { val selection = editor.selectionModel - return if (selection.hasSelection()) { + if (selection.hasSelection()) { // If there's an explicit selection, suggest only one expression - listOfNotNull(findExpressionInRange(file, selection.selectionStart, selection.selectionEnd)) + return listOfNotNull(findExpressionInRange(file, selection.selectionStart, selection.selectionEnd)) } else { val expr = findExpressionAtCaret(file, editor.caretModel.offset) @@ -185,20 +191,39 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { +fun findOccurrences(expr: PsiElement): List { val parent = expr.parentOfType(LatexFile::class) ?: return emptyList() return findOccurrences(parent, expr) } -fun findOccurrences(parent: PsiElement, expr: PsiElement): List { +fun findOccurrences(parent: PsiElement, expr: PsiElement): List { val visitor = object : PsiRecursiveElementVisitor() { val foundOccurrences = ArrayList() override fun visitElement(element: PsiElement) { @@ -246,12 +271,12 @@ fun findOccurrences(parent: PsiElement, expr: PsiElement): List { } } parent.acceptChildren(visitor) - return visitor.foundOccurrences + return visitor.foundOccurrences.map { LatexExtractablePSI(it) } } interface ExtractExpressionUi { - fun chooseTarget(exprs: List): PsiElement - fun chooseOccurrences(expr: PsiElement, occurrences: List): List + fun chooseTarget(exprs: List): LatexExtractablePSI + fun chooseOccurrences(expr: LatexExtractablePSI, occurrences: List): List } var MOCK: ExtractExpressionUi? = null diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt new file mode 100644 index 000000000..3a4a905c6 --- /dev/null +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt @@ -0,0 +1,12 @@ +package nl.hannahsten.texifyidea.refactoring.introduceCommand + +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement + +class LatexExtractablePSI( + val commonParent: PsiElement, + val extractableRange: TextRange = TextRange(0, commonParent.textLength) +) : PsiElement by commonParent { + +} + diff --git a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexInPlaceVariableIntroducer.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexInPlaceVariableIntroducer.kt similarity index 94% rename from src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexInPlaceVariableIntroducer.kt rename to src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexInPlaceVariableIntroducer.kt index 90a788fbf..1d288c5ee 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/myextractfunction/LatexInPlaceVariableIntroducer.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexInPlaceVariableIntroducer.kt @@ -1,4 +1,4 @@ -package nl.hannahsten.texifyidea.refactoring.myextractfunction +package nl.hannahsten.texifyidea.refactoring.introduceCommand import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project diff --git a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt index 492971e67..3379a60bc 100644 --- a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt +++ b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt @@ -3,8 +3,9 @@ package nl.hannahsten.texifyidea.refactoring import com.intellij.psi.PsiElement import com.intellij.testFramework.fixtures.BasePlatformTestCase import nl.hannahsten.texifyidea.file.LatexFileType -import nl.hannahsten.texifyidea.refactoring.myextractfunction.ExtractExpressionUi -import nl.hannahsten.texifyidea.refactoring.myextractfunction.withMockTargetExpressionChooser +import nl.hannahsten.texifyidea.refactoring.introduceCommand.ExtractExpressionUi +import nl.hannahsten.texifyidea.refactoring.introduceCommand.LatexExtractablePSI +import nl.hannahsten.texifyidea.refactoring.introduceCommand.withMockTargetExpressionChooser class IntroduceVariableTest : BasePlatformTestCase() { fun testBasicCaret() = doTest( @@ -163,13 +164,13 @@ class IntroduceVariableTest : BasePlatformTestCase() { ) { var shownTargetChooser = false withMockTargetExpressionChooser(object : ExtractExpressionUi { - override fun chooseTarget(exprs: List): PsiElement { + override fun chooseTarget(exprs: List): LatexExtractablePSI { shownTargetChooser = true assertEquals(exprs.map { it.text }, expressions) return exprs[target] } - override fun chooseOccurrences(expr: PsiElement, occurrences: List): List = + override fun chooseOccurrences(expr: LatexExtractablePSI, occurrences: List): List = if (replaceAll) occurrences else listOf(expr) }) { myFixture.configureByText(LatexFileType, before.trimIndent()) From 088837b8922926250f0023d1df49abff1ad03207 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:52:35 -0600 Subject: [PATCH 13/31] Highlighting MAGIC --- .../refactoring/introduceCommand/LatexExtractCommandHandler.kt | 3 ++- .../refactoring/introduceCommand/LatexExtractablePSI.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index 365515583..6bff62926 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -73,7 +73,8 @@ fun showExpressionChooser( if (isUnitTestMode) { callback(MOCK!!.chooseTarget(exprs)) } else - IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass) { it.text.substring(it.extractableRange.startOffset, it.extractableRange.endOffset) } + IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass, + { it.text.substring(it.extractableRange.startOffset, it.extractableRange.endOffset) }, RefactoringBundle.message("introduce.target.chooser.expressions.title"), { (it as LatexExtractablePSI).extractableTextRange }) } fun extractExpression( diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt index 3a4a905c6..d9ec1846a 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt @@ -2,11 +2,12 @@ package nl.hannahsten.texifyidea.refactoring.introduceCommand import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement +import com.intellij.refactoring.suggested.startOffset class LatexExtractablePSI( val commonParent: PsiElement, val extractableRange: TextRange = TextRange(0, commonParent.textLength) ) : PsiElement by commonParent { - + val extractableTextRange get() = TextRange(commonParent.startOffset + extractableRange.startOffset, commonParent.startOffset + extractableRange.endOffset) } From efbdb3f15b51f050d4b2735661b06ce1e5c51be2 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:16:16 -0600 Subject: [PATCH 14/31] Fix Int Range --- src/nl/hannahsten/texifyidea/util/General.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nl/hannahsten/texifyidea/util/General.kt b/src/nl/hannahsten/texifyidea/util/General.kt index 58ad29de9..62f93cb7a 100644 --- a/src/nl/hannahsten/texifyidea/util/General.kt +++ b/src/nl/hannahsten/texifyidea/util/General.kt @@ -71,7 +71,7 @@ fun IntRange.shiftRight(displacement: Int): IntRange { /** * Converts a [TextRange] to [IntRange]. */ -fun TextRange.toIntRange() = startOffset..endOffset +fun TextRange.toIntRange() = startOffset until endOffset /** * Easy access to [java.util.regex.Matcher.matches]. From f957f161e94e58eaf8693d6075636109f5e2c8e8 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Fri, 22 Sep 2023 15:19:22 -0600 Subject: [PATCH 15/31] Improve tests --- .../LatexExtractCommandHandler.kt | 104 +++++++++++------ src/nl/hannahsten/texifyidea/util/Commands.kt | 6 +- .../refactoring/IntroduceVariableTest.kt | 106 +++++++++--------- 3 files changed, 130 insertions(+), 86 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index 6bff62926..ab5acfad1 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -27,6 +27,7 @@ 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 nl.hannahsten.texifyidea.util.toIntRange import org.jetbrains.annotations.TestOnly class LatexExtractCommandHandler : RefactoringActionHandler { @@ -74,14 +75,13 @@ fun showExpressionChooser( callback(MOCK!!.chooseTarget(exprs)) } else IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass, - { it.text.substring(it.extractableRange.startOffset, it.extractableRange.endOffset) }, RefactoringBundle.message("introduce.target.chooser.expressions.title"), { (it as LatexExtractablePSI).extractableTextRange }) + { it.text.substring(it.extractableRange.toIntRange()) }, RefactoringBundle.message("introduce.target.chooser.expressions.title"), { (it as LatexExtractablePSI).extractableTextRange }) } fun extractExpression( editor: Editor, expr: LatexExtractablePSI, - @Suppress("UnstableApiUsage") - @NlsContexts.Command commandName: String + commandName: String ) { if (!expr.isValid) return val occurrences = findOccurrences(expr) @@ -94,39 +94,68 @@ fun extractExpression( private class ExpressionReplacer( private val project: Project, private val editor: Editor, - private val chosenExpr: PsiElement + private val chosenExpr: LatexExtractablePSI ) { private val psiFactory = LatexPsiHelper(project) fun replaceElementForAllExpr( - exprs: List, - @Suppress("UnstableApiUsage") - @NlsContexts.Command commandName: String + exprs: List, + 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) + if (chosenExpr.elementType == NORMAL_TEXT_WORD || chosenExpr.commonParent is LatexNormalText) { + runWriteCommandAction(project, commandName) { + val letBinding = insertCommandDefinition( + chosenExpr.containingFile, + chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()) + ) + ?: return@runWriteCommandAction //firstExpr.parent.addBefore(newcommand, firstExpr) + exprs.filter { it != chosenExpr }.forEach { + val newItem = it.text.replace(chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), "\\mycommand{}") + it.replace(psiFactory.createFromText(newItem).firstChild) + } + val newItem = chosenExpr.text.replace(chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), "\\mycommand{}") + chosenExpr.replace(psiFactory.createFromText(newItem).firstChild) - PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) - val filterIsInstance = - letBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() - val actualToken = - filterIsInstance.firstOrNull { it.text == "\\mycommand" } - ?: throw IllegalStateException("How did this happen??") + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) + val filterIsInstance = + letBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() + val actualToken = + filterIsInstance.firstOrNull { it.text == "\\mycommand" } + ?: throw IllegalStateException("How did this happen??") - editor.caretModel.moveToOffset(chosenInsertion.textRange.startOffset) + editor.caretModel.moveToOffset(actualToken.textRange.startOffset) - LatexInPlaceVariableIntroducer( - actualToken, editor, project, "choose a variable" - ) - .performInplaceRefactoring(LinkedHashSet()) + LatexInPlaceVariableIntroducer( + actualToken, editor, project, "choose a variable" + ) + .performInplaceRefactoring(LinkedHashSet()) + } + } else { + runWriteCommandAction(project, commandName) { + val letBinding = insertCommandDefinition( + chosenExpr.containingFile, + chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()) + ) + ?: return@runWriteCommandAction //firstExpr.parent.addBefore(newcommand, firstExpr) + exprs.filter { it != chosenExpr }.forEach { it.replace(name) } + val chosenInsertion = chosenExpr.replace(name) as LatexCommands + + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) + val filterIsInstance = + letBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() + val actualToken = + filterIsInstance.firstOrNull { it.text == "\\mycommand" } + ?: throw IllegalStateException("How did this happen??") + + editor.caretModel.moveToOffset(actualToken.textRange.startOffset) + + LatexInPlaceVariableIntroducer( + actualToken, editor, project, "choose a variable" + ) + .performInplaceRefactoring(LinkedHashSet()) + } } } } @@ -175,7 +204,7 @@ fun findExpressionInRange(file: PsiFile, startOffset: Int, endOffset: Int): Late val parent = findCommonParent(first, last) ?: return null return if (parent is LatexNormalText) { - LatexExtractablePSI(parent, TextRange(startOffset, endOffset)) + LatexExtractablePSI(parent, TextRange(startOffset - parent.startOffset, endOffset - parent.startOffset)) } else LatexExtractablePSI(parent) } @@ -207,18 +236,27 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { - val parent = expr.parentOfType(LatexFile::class) + val parent = expr.firstParentOfType(LatexFile::class) ?: return emptyList() return findOccurrences(parent, expr) } diff --git a/src/nl/hannahsten/texifyidea/util/Commands.kt b/src/nl/hannahsten/texifyidea/util/Commands.kt index b14af2d21..6f00d6e94 100644 --- a/src/nl/hannahsten/texifyidea/util/Commands.kt +++ b/src/nl/hannahsten/texifyidea/util/Commands.kt @@ -99,7 +99,7 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: val blockingNames = file.definitions().filter { it.commandToken.text.matches("${newCommandName}\\d*".toRegex()) } val nonConflictingName = "${newCommandName}${if (blockingNames.isEmpty()) "" else blockingNames.size.toString()}" - val command = "\\newcommand{\\$nonConflictingName}{${commandText}}"; + val command = "\\newcommand{\\$nonConflictingName}{${commandText}}" val newChild = LatexPsiHelper(file.project).createFromText(command).firstChild val newNode = newChild.node @@ -113,16 +113,18 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: PsiDocumentManager.getInstance(file.project).commitDocument(file.document() ?: return null) runWriteAction { + val newLine = LatexPsiHelper(file.project).createFromText("\n\n").firstChild.node // Avoid NPE, see #3083 (cause unknown) if (anchorAfter != null && com.intellij.psi.impl.source.tree.TreeUtil.getFileElement(anchorAfter.parent.node) != null) { val anchorBefore = anchorAfter.node.treeNext - val newLine = LatexPsiHelper(file.project).createFromText("\n\n").firstChild.node anchorAfter.parent.node.addChild(newLine, anchorBefore) anchorAfter.parent.node.addChild(newNode, anchorBefore) } else { // Insert at beginning + file.node.addChild(newLine, file.firstChild.node) file.node.addChild(newNode, file.firstChild.node) +// file.node.addChild(newLine, file.firstChild.node) } } diff --git a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt index 3379a60bc..74646be17 100644 --- a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt +++ b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt @@ -12,7 +12,9 @@ class IntroduceVariableTest : BasePlatformTestCase() { """ My favorite number is 5.25 """, listOf("5.25", "My favorite number is 5.25"), 0, """ - \newcommand{\mycommand}{5.25}My favorite number is \mycommand{} + \newcommand{\mycommand}{5.25} + + My favorite number is \mycommand{} """ ) @@ -20,7 +22,9 @@ class IntroduceVariableTest : BasePlatformTestCase() { """ My favorite number is 5.25 """, emptyList(), 0, """ - \newcommand{\mycommand}{5.25}My favorite number is \mycommand{} + \newcommand{\mycommand}{5.25} + + My favorite number is \mycommand{} """ ) @@ -38,33 +42,30 @@ class IntroduceVariableTest : BasePlatformTestCase() { Some old guy discovered 2.718 and was amazed! - Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} + Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} \end{document} """, listOf("2.718", "Some old guy discovered 2.718 and was amazed!", - """Some old guy discovered 2.718 and was amazed! - - Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'}""" + "Some old guy discovered 2.718 and was amazed!\n\n\t\tNot to be confused with 2.714, or ${'$'}3\\pi!-6\\pi${'$'}" ), 0, """ - \documentclass[11pt]{article} - - \newcommand{\mycommand}{2.718} - - \begin{document} - - \chapter{The significance of \mycommand{}, or e} - - \mycommand{} is a special number. - - ${'$'}\lim_{x \to \infty} (1+\frac{1}{x})^{x}=\mycommand{}${'$'} - - Some old guy discovered \mycommand{} and was amazed! - - Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} - - \end{document} - """, true - ) + \documentclass[11pt]{article} + + \newcommand{\mycommand}{2.718} + + \begin{document} + + \chapter{The significance of \mycommand{}, or e} + + \mycommand{} is a special number. + + ${'$'}\lim_{x \to \infty} (1+\frac{1}{x})^{x}=\mycommand{}${'$'} + + Some old guy discovered \mycommand{} and was amazed! + + Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} + + \end{document} + """, true) fun testMultiTableSelection() = doTest( """ @@ -94,7 +95,7 @@ class IntroduceVariableTest : BasePlatformTestCase() { """, emptyList(), 0, """ \documentclass[11pt]{article} - \newcommand{\mycommand}{} + \newcommand{\mycommand}{2.68291} \begin{document} @@ -118,42 +119,37 @@ class IntroduceVariableTest : BasePlatformTestCase() { Some may wonder why \mycommand{} is so special. \end{document} - """, true - ) + """, true) - fun testWithQuotes() = doTest( - """ + fun testWithQuotes() = doTest(""" We could not find strategies that would be of great assistance in this category. - However, if you ever find yourself reading ``Test With Quotes'' I thinnk you for your service. + However, if you ever find yourself reading ``Test With Quotes'' I thinnk you for your service. """, emptyList(), 0, """ \newcommand{\mycommand}{Test With Quotes} We could not find strategies that would be of great assistance in this category. - However, if you ever find yourself reading ``\mycommand{}'' I thinnk you for your service. - """ - ) + However, if you ever find yourself reading ``\mycommand{}'' I thinnk you for your service. + """) - fun testEnvironmentEnumerate() = doTest( - """ - Hello Werld - - \begin{enumerate} - \item{Page Data: page id, namespace, title (File Schema: enwiki-latest-page.sql.gz)} - \item{Link Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-pagelinks.sql.gz)} - \item{Redirect Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-redirect.sql.gz)} - \end{enumerate} + fun testEnvironmentEnumerate() = doTest(""" + Hello Werld + + \begin{enumerate} + \item{Page Data: page id, namespace, title (File Schema: enwiki-latest-page.sql.gz)} + \item{Link Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-pagelinks.sql.gz)} + \item{Redirect Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-redirect.sql.gz)} + \end{enumerate} """, emptyList(), 0, """ \newcommand{\mycommand}{\begin{enumerate} - \item{Page Data: page id, namespace, title (File Schema: enwiki-latest-page.sql.gz)} - \item{Link Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-pagelinks.sql.gz)} - \item{Redirect Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-redirect.sql.gz)} - \end{enumerate}} - + \item{Page Data: page id, namespace, title (File Schema: enwiki-latest-page.sql.gz)} + \item{Link Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-pagelinks.sql.gz)} + \item{Redirect Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-redirect.sql.gz)} + \end{enumerate}} + Hello Werld \mycommand{} - """ - ) + """) private fun doTest( before: String, @@ -166,7 +162,11 @@ class IntroduceVariableTest : BasePlatformTestCase() { withMockTargetExpressionChooser(object : ExtractExpressionUi { override fun chooseTarget(exprs: List): LatexExtractablePSI { shownTargetChooser = true - assertEquals(exprs.map { it.text }, expressions) + println("saw") + exprs.forEach { println("'" + it.text + "'") } + println("xpect") + expressions.map { println("'" + it + "'") } + assertEquals(exprs.map { it.text.trimIndent() }, expressions.map { it.trimIndent() }) return exprs[target] } @@ -174,6 +174,10 @@ class IntroduceVariableTest : BasePlatformTestCase() { if (replaceAll) occurrences else listOf(expr) }) { myFixture.configureByText(LatexFileType, before.trimIndent()) +/* println("'" + before + "'") + println(before.trimIndent()) + println("'" + after + "'") + println(after.trimIndent())*/ myFixture.performEditorAction("IntroduceVariable") myFixture.checkResult(after.trimIndent()) From 80d8f4e37c04270479668422304d12afad3bb24c Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:26:17 -0600 Subject: [PATCH 16/31] Dont offer something twice --- .../introduceCommand/LatexExtractCommandHandler.kt | 2 +- .../texifyidea/refactoring/IntroduceVariableTest.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index ab5acfad1..3cdececf4 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -255,7 +255,7 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List Date: Sat, 23 Sep 2023 16:11:15 -0600 Subject: [PATCH 17/31] distinct better --- .../introduceCommand/LatexExtractCommandHandler.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index 3cdececf4..10ca17f7e 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -235,7 +235,7 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List Date: Sun, 24 Sep 2023 10:24:21 +0200 Subject: [PATCH 18/31] Add comment to LatexExtractCommandHandler --- .../introduceCommand/LatexExtractCommandHandler.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index 10ca17f7e..251b71155 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -30,6 +30,12 @@ import nl.hannahsten.texifyidea.util.runWriteCommandAction import nl.hannahsten.texifyidea.util.toIntRange import org.jetbrains.annotations.TestOnly +/** + * Extract the selected piece of text into a \newcommand definition and replace usages. + * + * Based on code from https://github.com/intellij-rust/intellij-rust/blob/b18aab90317564307829f3c9c8e0188817a377ad/src/main/kotlin/org/rust/ide/refactoring/extraxtExpressionUi.kt#L1 + * and https://github.com/intellij-rust/intellij-rust/blob/b18aab90317564307829f3c9c8e0188817a377ad/src/main/kotlin/org/rust/ide/refactoring/extraxtExpressionUtils.kt#L1 + */ class LatexExtractCommandHandler : RefactoringActionHandler { override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) { if (file !is LatexFile) return From 7b42ab333824b16ea9a6a1c2cca68b3866536514 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Sun, 24 Sep 2023 10:58:01 -0600 Subject: [PATCH 19/31] Fix test --- .../introduceCommand/LatexExtractCommandHandler.kt | 10 +++++----- .../texifyidea/refactoring/IntroduceVariableTest.kt | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index 251b71155..597838677 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -264,11 +264,11 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List): LatexExtractablePSI { shownTargetChooser = true println("saw") - exprs.forEach { println("'" + it.text + "'") } + exprs.forEach { println("'" + it.text.substring(it.extractableRange.toIntRange()) + "'") } println("xpect") expressions.map { println("'" + it + "'") } - assertEquals(exprs.map { it.text.trimIndent() }, expressions.map { it.trimIndent() }) + assertEquals(exprs.map { it.text.substring(it.extractableRange.toIntRange()).trimIndent() }, expressions.map { it.trimIndent() }) return exprs[target] } From a044540ea66e538c3f36880aee6b23a426aeab51 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:03:34 -0600 Subject: [PATCH 20/31] All tests passing --- .../LatexExtractCommandHandler.kt | 81 +++++++++++++------ src/nl/hannahsten/texifyidea/util/Commands.kt | 17 ++-- .../refactoring/IntroduceVariableTest.kt | 78 ++++++++++++------ 3 files changed, 121 insertions(+), 55 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index 597838677..0e5ed0acb 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -5,14 +5,12 @@ import com.intellij.ide.plugins.PluginManagerCore.isUnitTestMode import com.intellij.openapi.actionSystem.DataContext 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.openapi.util.TextRange 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.parentOfType import com.intellij.psi.util.parents import com.intellij.refactoring.IntroduceTargetChooser import com.intellij.refactoring.RefactoringActionHandler @@ -25,7 +23,10 @@ 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.parser.childrenOfType +import nl.hannahsten.texifyidea.util.parser.endCommand +import nl.hannahsten.texifyidea.util.parser.firstChildOfType +import nl.hannahsten.texifyidea.util.parser.firstParentOfType import nl.hannahsten.texifyidea.util.runWriteCommandAction import nl.hannahsten.texifyidea.util.toIntRange import org.jetbrains.annotations.TestOnly @@ -79,9 +80,16 @@ fun showExpressionChooser( ) { if (isUnitTestMode) { callback(MOCK!!.chooseTarget(exprs)) - } else - IntroduceTargetChooser.showChooser(editor, exprs, callback.asPass, - { it.text.substring(it.extractableRange.toIntRange()) }, RefactoringBundle.message("introduce.target.chooser.expressions.title"), { (it as LatexExtractablePSI).extractableTextRange }) + } + else + IntroduceTargetChooser.showChooser( + editor, + exprs, + callback.asPass, + { it.text.substring(it.extractableRange.toIntRange()) }, + RefactoringBundle.message("introduce.target.chooser.expressions.title"), + { (it as LatexExtractablePSI).extractableTextRange } + ) } fun extractExpression( @@ -110,23 +118,38 @@ private class ExpressionReplacer( ) { val name = psiFactory.createFromText("\\mycommand{}").firstChildOfType(LatexCommands::class) ?: return + val containingFile = chosenExpr.containingFile if (chosenExpr.elementType == NORMAL_TEXT_WORD || chosenExpr.commonParent is LatexNormalText) { runWriteCommandAction(project, commandName) { val letBinding = insertCommandDefinition( - chosenExpr.containingFile, + containingFile, chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()) ) - ?: return@runWriteCommandAction //firstExpr.parent.addBefore(newcommand, firstExpr) + ?: return@runWriteCommandAction exprs.filter { it != chosenExpr }.forEach { - val newItem = it.text.replace(chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), "\\mycommand{}") + val newItem = it.text.replace( + chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), + "\\mycommand{}" + ) it.replace(psiFactory.createFromText(newItem).firstChild) } - val newItem = chosenExpr.text.replace(chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), "\\mycommand{}") + val newItem = chosenExpr.text.replace( + chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), + "\\mycommand{}" + ) chosenExpr.replace(psiFactory.createFromText(newItem).firstChild) + val letOffset = letBinding.textRange + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) + + println("you have beautiful eyes") + + val respawnedLetBinding = findExpressionAtCaret(containingFile as LatexFile, letOffset.startOffset) + ?: throw IllegalStateException("This really sux") + val filterIsInstance = - letBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() + respawnedLetBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() val actualToken = filterIsInstance.firstOrNull { it.text == "\\mycommand" } ?: throw IllegalStateException("How did this happen??") @@ -138,15 +161,16 @@ private class ExpressionReplacer( ) .performInplaceRefactoring(LinkedHashSet()) } - } else { + } + else { runWriteCommandAction(project, commandName) { val letBinding = insertCommandDefinition( - chosenExpr.containingFile, + containingFile, chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()) ) - ?: return@runWriteCommandAction //firstExpr.parent.addBefore(newcommand, firstExpr) + ?: return@runWriteCommandAction exprs.filter { it != chosenExpr }.forEach { it.replace(name) } - val chosenInsertion = chosenExpr.replace(name) as LatexCommands + chosenExpr.replace(name) as LatexCommands PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) val filterIsInstance = @@ -174,7 +198,8 @@ fun showOccurrencesChooser( ) { if (isUnitTestMode && occurrences.size > 1) { callback(MOCK!!.chooseOccurrences(expr, occurrences)) - } else { + } + else { OccurrencesChooser.simpleChooser(editor) .showChooser( expr, @@ -211,7 +236,8 @@ fun findExpressionInRange(file: PsiFile, startOffset: Int, endOffset: Int): Late return if (parent is LatexNormalText) { LatexExtractablePSI(parent, TextRange(startOffset - parent.startOffset, endOffset - parent.startOffset)) - } else + } + else LatexExtractablePSI(parent) } @@ -235,17 +261,22 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List Unit) { MOCK = mock try { f() - } finally { + } + finally { MOCK = null } } diff --git a/src/nl/hannahsten/texifyidea/util/Commands.kt b/src/nl/hannahsten/texifyidea/util/Commands.kt index 6f00d6e94..f75c78392 100644 --- a/src/nl/hannahsten/texifyidea/util/Commands.kt +++ b/src/nl/hannahsten/texifyidea/util/Commands.kt @@ -1,26 +1,22 @@ package nl.hannahsten.texifyidea.util import com.intellij.openapi.project.Project -import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.search.GlobalSearchScope import nl.hannahsten.texifyidea.index.LatexCommandsIndex import nl.hannahsten.texifyidea.index.LatexDefinitionIndex -import nl.hannahsten.texifyidea.lang.LatexPackage import nl.hannahsten.texifyidea.lang.commands.LatexCommand import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand import nl.hannahsten.texifyidea.lang.commands.RequiredFileArgument import nl.hannahsten.texifyidea.psi.LatexCommands import nl.hannahsten.texifyidea.psi.LatexParameter import nl.hannahsten.texifyidea.psi.LatexPsiHelper -import nl.hannahsten.texifyidea.settings.TexifySettings import nl.hannahsten.texifyidea.util.files.* import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands import nl.hannahsten.texifyidea.util.magic.CommandMagic import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic -import nl.hannahsten.texifyidea.util.magic.PackageMagic import nl.hannahsten.texifyidea.util.parser.* import java.util.stream.Collectors @@ -64,9 +60,11 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: for (cmd in commands) { if (cmd.commandToken.text == "\\newcommand") { last = cmd - } else if (cmd.commandToken.text == "\\usepackage") { + } + else if (cmd.commandToken.text == "\\usepackage") { last = cmd - } else if (cmd.commandToken.text == "\\begin" && cmd.requiredParameter(0) == "document") { + } + else if (cmd.commandToken.text == "\\begin" && cmd.requiredParameter(0) == "document") { last = cmd break } @@ -99,7 +97,7 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: val blockingNames = file.definitions().filter { it.commandToken.text.matches("${newCommandName}\\d*".toRegex()) } val nonConflictingName = "${newCommandName}${if (blockingNames.isEmpty()) "" else blockingNames.size.toString()}" - val command = "\\newcommand{\\$nonConflictingName}{${commandText}}" + val command = "\\newcommand{\\$nonConflictingName}{${commandText}}\n" val newChild = LatexPsiHelper(file.project).createFromText(command).firstChild val newNode = newChild.node @@ -141,7 +139,10 @@ fun expandCommandsOnce(inputText: String, project: Project, file: PsiFile?): Str for (command in commandsInText) { // Expand the command once, and replace the command with the expanded text - val commandExpansion = LatexCommandsIndex.getCommandsByNames(file ?: return null, *CommandMagic.commandDefinitionsAndRedefinitions.toTypedArray()) + val commandExpansion = LatexCommandsIndex.getCommandsByNames( + file ?: return null, + *CommandMagic.commandDefinitionsAndRedefinitions.toTypedArray() + ) .firstOrNull { it.getRequiredArgumentValueByName("cmd") == command.text } ?.getRequiredArgumentValueByName("def") text = text.replace(command.text, commandExpansion ?: command.text) diff --git a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt index 15f996929..b49446b06 100644 --- a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt +++ b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt @@ -1,6 +1,5 @@ package nl.hannahsten.texifyidea.refactoring -import com.intellij.psi.PsiElement import com.intellij.testFramework.fixtures.BasePlatformTestCase import nl.hannahsten.texifyidea.file.LatexFileType import nl.hannahsten.texifyidea.refactoring.introduceCommand.ExtractExpressionUi @@ -12,7 +11,10 @@ class IntroduceVariableTest : BasePlatformTestCase() { fun testBasicCaret() = doTest( """ My favorite number is 5.25 - """, listOf("5.25", "My favorite number is 5.25"), 0, """ + """, + listOf("5.25", "My favorite number is 5.25"), + 0, + """ \newcommand{\mycommand}{5.25} My favorite number is \mycommand{} @@ -22,7 +24,10 @@ class IntroduceVariableTest : BasePlatformTestCase() { fun testBasicSelection() = doTest( """ My favorite number is 5.25 - """, emptyList(), 0, """ + """, + emptyList(), + 0, + """ \newcommand{\mycommand}{5.25} My favorite number is \mycommand{} @@ -46,9 +51,14 @@ class IntroduceVariableTest : BasePlatformTestCase() { Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} \end{document} - """, listOf("2.718", "Some old guy discovered 2.718 and was amazed!", - "Some old guy discovered 2.718 and was amazed!\n\n Not to be confused with 2.714, or" - ), 0, """ + """, + listOf( + "2.718", + "Some old guy discovered 2.718 and was amazed!", + "Some old guy discovered 2.718 and was amazed!\n\n Not to be confused with 2.714, or" + ), + 0, + """ \documentclass[11pt]{article} \newcommand{\mycommand}{2.718} @@ -66,7 +76,9 @@ class IntroduceVariableTest : BasePlatformTestCase() { Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} \end{document} - """, true) + """, + true + ) fun testMultiTableSelection() = doTest( """ @@ -93,11 +105,13 @@ class IntroduceVariableTest : BasePlatformTestCase() { Some may wonder why 2.68291 is so special. \end{document} - """, emptyList(), 0, """ + """, + emptyList(), + 0, + """ \documentclass[11pt]{article} \newcommand{\mycommand}{2.68291} - \begin{document} ${'$'}5.25 * \mycommand{} = 450${'$'} @@ -113,26 +127,34 @@ class IntroduceVariableTest : BasePlatformTestCase() { \begin{table}[ht!] \begin{tabular}{| r |} \hline - \mycommand{} \\ + \mycommand{} \\ \hline \end{tabular} \end{table} Some may wonder why \mycommand{} is so special. \end{document} - """, true) + """, + true + ) - fun testWithQuotes() = doTest(""" + fun testWithQuotes() = doTest( + """ We could not find strategies that would be of great assistance in this category. However, if you ever find yourself reading ``Test With Quotes'' I thinnk you for your service. - """, emptyList(), 0, """ + """, + emptyList(), + 0, + """ \newcommand{\mycommand}{Test With Quotes} We could not find strategies that would be of great assistance in this category. However, if you ever find yourself reading ``\mycommand{}'' I thinnk you for your service. - """) + """ + ) - fun testEnvironmentEnumerate() = doTest(""" + fun testEnvironmentEnumerate() = doTest( + """ Hello Werld \begin{enumerate} @@ -140,7 +162,10 @@ class IntroduceVariableTest : BasePlatformTestCase() { \item{Link Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-pagelinks.sql.gz)} \item{Redirect Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-redirect.sql.gz)} \end{enumerate} - """, emptyList(), 0, """ + """, + emptyList(), + 0, + """ \newcommand{\mycommand}{\begin{enumerate} \item{Page Data: page id, namespace, title (File Schema: enwiki-latest-page.sql.gz)} \item{Link Data: originating page, originating namespace, target page, target namespace (File Schema: enwiki-latest-pagelinks.sql.gz)} @@ -150,7 +175,8 @@ class IntroduceVariableTest : BasePlatformTestCase() { Hello Werld \mycommand{} - """) + """ + ) private fun doTest( before: String, @@ -167,18 +193,24 @@ class IntroduceVariableTest : BasePlatformTestCase() { exprs.forEach { println("'" + it.text.substring(it.extractableRange.toIntRange()) + "'") } println("xpect") expressions.map { println("'" + it + "'") } - assertEquals(exprs.map { it.text.substring(it.extractableRange.toIntRange()).trimIndent() }, expressions.map { it.trimIndent() }) + assertEquals( + exprs.map { it.text.substring(it.extractableRange.toIntRange()).trimIndent() }, + expressions.map { it.trimIndent() } + ) return exprs[target] } - override fun chooseOccurrences(expr: LatexExtractablePSI, occurrences: List): List = + override fun chooseOccurrences( + expr: LatexExtractablePSI, + occurrences: List + ): List = if (replaceAll) occurrences else listOf(expr) }) { myFixture.configureByText(LatexFileType, before.trimIndent()) -/* println("'" + before + "'") - println(before.trimIndent()) - println("'" + after + "'") - println(after.trimIndent())*/ + /* println("'" + before + "'") + println(before.trimIndent()) + println("'" + after + "'") + println(after.trimIndent())*/ myFixture.performEditorAction("IntroduceVariable") myFixture.checkResult(after.trimIndent()) From 77492400e2c0fcb415c88d78e058d52916a16975 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:30:32 -0600 Subject: [PATCH 21/31] Lint --- .../refactoring/introduceCommand/LatexExtractablePSI.kt | 1 - src/nl/hannahsten/texifyidea/util/Commands.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt index d9ec1846a..3c5335d4c 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt @@ -10,4 +10,3 @@ class LatexExtractablePSI( ) : PsiElement by commonParent { val extractableTextRange get() = TextRange(commonParent.startOffset + extractableRange.startOffset, commonParent.startOffset + extractableRange.endOffset) } - diff --git a/src/nl/hannahsten/texifyidea/util/Commands.kt b/src/nl/hannahsten/texifyidea/util/Commands.kt index f75c78392..8dcd52548 100644 --- a/src/nl/hannahsten/texifyidea/util/Commands.kt +++ b/src/nl/hannahsten/texifyidea/util/Commands.kt @@ -97,7 +97,7 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: val blockingNames = file.definitions().filter { it.commandToken.text.matches("${newCommandName}\\d*".toRegex()) } val nonConflictingName = "${newCommandName}${if (blockingNames.isEmpty()) "" else blockingNames.size.toString()}" - val command = "\\newcommand{\\$nonConflictingName}{${commandText}}\n" + val command = "\\newcommand{\\$nonConflictingName}{$commandText}\n" val newChild = LatexPsiHelper(file.project).createFromText(command).firstChild val newNode = newChild.node From c68f42dfc1957e2dc8d7e9613575649f005d302a Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Sat, 30 Sep 2023 21:31:48 -0600 Subject: [PATCH 22/31] Fix parameter error --- .../refactoring/introduceCommand/LatexExtractCommandHandler.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index 0e5ed0acb..a45a1a701 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -268,6 +268,7 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List Date: Sat, 30 Sep 2023 21:34:07 -0600 Subject: [PATCH 23/31] Remove unnecessary `{}` --- .../introduceCommand/LatexExtractCommandHandler.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index a45a1a701..0edb190e5 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -116,7 +116,7 @@ private class ExpressionReplacer( exprs: List, commandName: String ) { - val name = psiFactory.createFromText("\\mycommand{}").firstChildOfType(LatexCommands::class) ?: return + val name = psiFactory.createFromText("\\mycommand ").firstChildOfType(LatexCommands::class) ?: return val containingFile = chosenExpr.containingFile if (chosenExpr.elementType == NORMAL_TEXT_WORD || chosenExpr.commonParent is LatexNormalText) { @@ -129,13 +129,13 @@ private class ExpressionReplacer( exprs.filter { it != chosenExpr }.forEach { val newItem = it.text.replace( chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), - "\\mycommand{}" + "\\mycommand " ) it.replace(psiFactory.createFromText(newItem).firstChild) } val newItem = chosenExpr.text.replace( chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), - "\\mycommand{}" + "\\mycommand " ) chosenExpr.replace(psiFactory.createFromText(newItem).firstChild) From a229ddea83d3220b0540ba7df5bf99c6d1554d72 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Sat, 30 Sep 2023 21:42:09 -0600 Subject: [PATCH 24/31] Pass tests By coercion of test cases or actual fixes --- .../LatexExtractCommandHandler.kt | 6 ++--- .../refactoring/IntroduceVariableTest.kt | 24 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt index 0edb190e5..302ae8632 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt @@ -116,7 +116,7 @@ private class ExpressionReplacer( exprs: List, commandName: String ) { - val name = psiFactory.createFromText("\\mycommand ").firstChildOfType(LatexCommands::class) ?: return + val name = psiFactory.createFromText("\\mycommand").firstChildOfType(LatexCommands::class) ?: return val containingFile = chosenExpr.containingFile if (chosenExpr.elementType == NORMAL_TEXT_WORD || chosenExpr.commonParent is LatexNormalText) { @@ -129,13 +129,13 @@ private class ExpressionReplacer( exprs.filter { it != chosenExpr }.forEach { val newItem = it.text.replace( chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), - "\\mycommand " + "\\mycommand" ) it.replace(psiFactory.createFromText(newItem).firstChild) } val newItem = chosenExpr.text.replace( chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), - "\\mycommand " + "\\mycommand" ) chosenExpr.replace(psiFactory.createFromText(newItem).firstChild) diff --git a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt index b49446b06..903791413 100644 --- a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt +++ b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt @@ -17,7 +17,7 @@ class IntroduceVariableTest : BasePlatformTestCase() { """ \newcommand{\mycommand}{5.25} - My favorite number is \mycommand{} + My favorite number is \mycommand """ ) @@ -30,7 +30,7 @@ class IntroduceVariableTest : BasePlatformTestCase() { """ \newcommand{\mycommand}{5.25} - My favorite number is \mycommand{} + My favorite number is \mycommand """ ) @@ -65,13 +65,13 @@ class IntroduceVariableTest : BasePlatformTestCase() { \begin{document} - \chapter{The significance of \mycommand{}, or e} + \chapter{The significance of \mycommand, or e} - \mycommand{} is a special number. + \mycommand is a special number. - ${'$'}\lim_{x \to \infty} (1+\frac{1}{x})^{x}=\mycommand{}${'$'} + ${'$'}\lim_{x \to \infty} (1+\frac{1}{x})^{x}=\mycommand${'$'} - Some old guy discovered \mycommand{} and was amazed! + Some old guy discovered \mycommand and was amazed! Not to be confused with 2.714, or ${'$'}3\pi!-6\pi${'$'} @@ -114,12 +114,12 @@ class IntroduceVariableTest : BasePlatformTestCase() { \newcommand{\mycommand}{2.68291} \begin{document} - ${'$'}5.25 * \mycommand{} = 450${'$'} + ${'$'}5.25 * \mycommand = 450${'$'} \begin{table}[ht!] \begin{tabular}{| r |} \hline - \mycommand{} \\ + \mycommand \\ \hline \end{tabular} \end{table} @@ -127,12 +127,12 @@ class IntroduceVariableTest : BasePlatformTestCase() { \begin{table}[ht!] \begin{tabular}{| r |} \hline - \mycommand{} \\ + \mycommand \\ \hline \end{tabular} \end{table} - Some may wonder why \mycommand{} is so special. + Some may wonder why \mycommand is so special. \end{document} """, true @@ -149,7 +149,7 @@ class IntroduceVariableTest : BasePlatformTestCase() { \newcommand{\mycommand}{Test With Quotes} We could not find strategies that would be of great assistance in this category. - However, if you ever find yourself reading ``\mycommand{}'' I thinnk you for your service. + However, if you ever find yourself reading ``\mycommand'' I thinnk you for your service. """ ) @@ -174,7 +174,7 @@ class IntroduceVariableTest : BasePlatformTestCase() { Hello Werld - \mycommand{} + \mycommand """ ) From dc610aeac54e5ac1403e90d4d605244aadf7be42 Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:55:28 -0600 Subject: [PATCH 25/31] Distribute utilities --- .../LatexRefactoringSupportProvider.kt | 2 +- .../introduceCommand/LatexExtractablePSI.kt | 12 -- .../LatexExtractCommandHandler.kt | 135 ++++-------------- .../LatexInPlaceVariableIntroducer.kt | 4 +- .../texifyidea/util/files/PsiFile.kt | 71 +++++++-- .../util/parser/LatexExtractablePSI.kt | 33 +++++ .../texifyidea/util/parser/LatexPsiUtil.kt | 30 +++- .../refactoring/IntroduceVariableTest.kt | 11 +- 8 files changed, 156 insertions(+), 142 deletions(-) delete mode 100644 src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt rename src/nl/hannahsten/texifyidea/refactoring/{introduceCommand => introducecommand}/LatexExtractCommandHandler.kt (71%) rename src/nl/hannahsten/texifyidea/refactoring/{introduceCommand => introducecommand}/LatexInPlaceVariableIntroducer.kt (88%) create mode 100644 src/nl/hannahsten/texifyidea/util/parser/LatexExtractablePSI.kt diff --git a/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt b/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt index 673b9938c..7fe14e979 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/LatexRefactoringSupportProvider.kt @@ -4,7 +4,7 @@ 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.introduceCommand.LatexExtractCommandHandler +import nl.hannahsten.texifyidea.refactoring.introducecommand.LatexExtractCommandHandler /** * This class is used to enable inline refactoring. diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt b/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt deleted file mode 100644 index 3c5335d4c..000000000 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractablePSI.kt +++ /dev/null @@ -1,12 +0,0 @@ -package nl.hannahsten.texifyidea.refactoring.introduceCommand - -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiElement -import com.intellij.refactoring.suggested.startOffset - -class LatexExtractablePSI( - val commonParent: PsiElement, - val extractableRange: TextRange = TextRange(0, commonParent.textLength) -) : PsiElement by commonParent { - val extractableTextRange get() = TextRange(commonParent.startOffset + extractableRange.startOffset, commonParent.startOffset + extractableRange.endOffset) -} diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt similarity index 71% rename from src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt rename to src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt index 302ae8632..4ccb58beb 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt @@ -1,6 +1,5 @@ -package nl.hannahsten.texifyidea.refactoring.introduceCommand +package nl.hannahsten.texifyidea.refactoring.introducecommand -import com.intellij.codeInsight.PsiEquivalenceUtil import com.intellij.ide.plugins.PluginManagerCore.isUnitTestMode import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor @@ -8,7 +7,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Pass import com.intellij.openapi.util.TextRange 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 @@ -16,19 +14,16 @@ 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.files.findExpressionAtCaret +import nl.hannahsten.texifyidea.util.files.findExpressionInRange import nl.hannahsten.texifyidea.util.insertCommandDefinition -import nl.hannahsten.texifyidea.util.parser.childrenOfType -import nl.hannahsten.texifyidea.util.parser.endCommand -import nl.hannahsten.texifyidea.util.parser.firstChildOfType -import nl.hannahsten.texifyidea.util.parser.firstParentOfType +import nl.hannahsten.texifyidea.util.parser.* import nl.hannahsten.texifyidea.util.runWriteCommandAction -import nl.hannahsten.texifyidea.util.toIntRange import org.jetbrains.annotations.TestOnly /** @@ -86,9 +81,9 @@ fun showExpressionChooser( editor, exprs, callback.asPass, - { it.text.substring(it.extractableRange.toIntRange()) }, + { it.text.substring(it.extractableIntRange) }, RefactoringBundle.message("introduce.target.chooser.expressions.title"), - { (it as LatexExtractablePSI).extractableTextRange } + { (it as LatexExtractablePSI).extractableRangeInFile } ) } @@ -98,7 +93,7 @@ fun extractExpression( commandName: String ) { if (!expr.isValid) return - val occurrences = findOccurrences(expr) + val occurrences = expr.findOccurrences() showOccurrencesChooser(editor, expr, occurrences) { occurrencesToReplace -> ExpressionReplacer(expr.project, editor, expr) .replaceElementForAllExpr(occurrencesToReplace, commandName) @@ -116,25 +111,23 @@ private class ExpressionReplacer( exprs: List, commandName: String ) { - val name = psiFactory.createFromText("\\mycommand").firstChildOfType(LatexCommands::class) ?: return - val containingFile = chosenExpr.containingFile - if (chosenExpr.elementType == NORMAL_TEXT_WORD || chosenExpr.commonParent is LatexNormalText) { + if (chosenExpr.elementType == NORMAL_TEXT_WORD || chosenExpr.self is LatexNormalText) { runWriteCommandAction(project, commandName) { val letBinding = insertCommandDefinition( containingFile, - chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()) + chosenExpr.text.substring(chosenExpr.extractableIntRange) ) ?: return@runWriteCommandAction exprs.filter { it != chosenExpr }.forEach { val newItem = it.text.replace( - chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), + chosenExpr.text.substring(chosenExpr.extractableIntRange), "\\mycommand" ) it.replace(psiFactory.createFromText(newItem).firstChild) } val newItem = chosenExpr.text.replace( - chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()), + chosenExpr.text.substring(chosenExpr.extractableIntRange), "\\mycommand" ) chosenExpr.replace(psiFactory.createFromText(newItem).firstChild) @@ -145,7 +138,7 @@ private class ExpressionReplacer( println("you have beautiful eyes") - val respawnedLetBinding = findExpressionAtCaret(containingFile as LatexFile, letOffset.startOffset) + val respawnedLetBinding = (containingFile as LatexFile).findExpressionAtCaret(letOffset.startOffset) ?: throw IllegalStateException("This really sux") val filterIsInstance = @@ -164,9 +157,12 @@ private class ExpressionReplacer( } else { runWriteCommandAction(project, commandName) { + val name = psiFactory.createFromText("\\mycommand").firstChildOfType(LatexCommands::class) + ?: return@runWriteCommandAction + val letBinding = insertCommandDefinition( containingFile, - chosenExpr.text.substring(chosenExpr.extractableRange.toIntRange()) + chosenExpr.text.substring(chosenExpr.extractableIntRange) ) ?: return@runWriteCommandAction exprs.filter { it != chosenExpr }.forEach { it.replace(name) } @@ -212,65 +208,42 @@ fun showOccurrencesChooser( } } +// Pass is deprecated, but IntroduceTargetChooser.showChooser doesnt have compatible signatures to replace with consumer yet private val ((T) -> Unit).asPass: Pass get() = object : Pass() { override fun pass(t: T) = this@asPass(t) } -fun findExpressionInRange(file: PsiFile, startOffset: Int, endOffset: Int): LatexExtractablePSI? { - 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 - - val parent = findCommonParent(first, last) ?: return null - - return if (parent is LatexNormalText) { - LatexExtractablePSI(parent, TextRange(startOffset - parent.startOffset, endOffset - parent.startOffset)) - } - else - LatexExtractablePSI(parent) -} - fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { val selection = editor.selectionModel if (selection.hasSelection()) { // If there's an explicit selection, suggest only one expression - return listOfNotNull(findExpressionInRange(file, selection.selectionStart, selection.selectionEnd)) + return listOfNotNull(file.findExpressionInRange(selection.selectionStart, selection.selectionEnd)) } else { - val expr = findExpressionAtCaret(file, editor.caretModel.offset) + val expr = file.findExpressionAtCaret(editor.caretModel.offset) ?: return emptyList() if (expr is LatexBeginCommand) { val endCommand = expr.endCommand() - if (endCommand == null) - return emptyList() + return if (endCommand == null) + emptyList() else { val environToken = findCommonParent(expr, endCommand) - return if (environToken != null) - listOf(LatexExtractablePSI(environToken)) + if (environToken != null) + listOf(environToken.asExtractable()) else emptyList() } } else if (expr is LatexNormalText) { - return listOf(LatexExtractablePSI(expr)) + return listOf(expr.asExtractable()) } else { if (expr.elementType == NORMAL_TEXT_WORD) { val interruptedParent = expr.firstParentOfType(LatexNormalText::class) ?: expr.firstParentOfType(LatexParameterText::class) ?: throw IllegalStateException("You suck") - var out = arrayListOf(LatexExtractablePSI(expr)) + val out = arrayListOf(expr.asExtractable()) val interruptedText = interruptedParent.text if (interruptedText.contains('\n')) { val previousLineBreak = @@ -283,75 +256,29 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List 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 { - val parent = expr.firstParentOfType(LatexFile::class) - ?: return emptyList() - return findOccurrences(parent, expr) -} - -fun findOccurrences(parent: PsiElement, expr: PsiElement): List { - val visitor = object : PsiRecursiveElementVisitor() { - val foundOccurrences = ArrayList() - override fun visitElement(element: PsiElement) { - if (PsiEquivalenceUtil.areElementsEquivalent(expr, element)) { - foundOccurrences.add(element) - } - else { - super.visitElement(element) - } - } - } - parent.acceptChildren(visitor) - return visitor.foundOccurrences.map { LatexExtractablePSI(it) } -} - interface ExtractExpressionUi { fun chooseTarget(exprs: List): LatexExtractablePSI fun chooseOccurrences(expr: LatexExtractablePSI, occurrences: List): List diff --git a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexInPlaceVariableIntroducer.kt b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexInPlaceVariableIntroducer.kt similarity index 88% rename from src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexInPlaceVariableIntroducer.kt rename to src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexInPlaceVariableIntroducer.kt index 1d288c5ee..a5a1a9e72 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introduceCommand/LatexInPlaceVariableIntroducer.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexInPlaceVariableIntroducer.kt @@ -1,4 +1,4 @@ -package nl.hannahsten.texifyidea.refactoring.introduceCommand +package nl.hannahsten.texifyidea.refactoring.introducecommand import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project @@ -13,7 +13,7 @@ class LatexInPlaceVariableIntroducer( elementToRename: PsiNamedElement, editor: Editor, project: Project, - @Suppress("UnstableApiUsage") @NlsContexts.Command title: String, + @NlsContexts.Command title: String, private val additionalElementsToRename: List = emptyList() ) : InplaceVariableIntroducer(elementToRename, editor, project, title, emptyArray(), null) { diff --git a/src/nl/hannahsten/texifyidea/util/files/PsiFile.kt b/src/nl/hannahsten/texifyidea/util/files/PsiFile.kt index f55a54e00..e830d6f83 100644 --- a/src/nl/hannahsten/texifyidea/util/files/PsiFile.kt +++ b/src/nl/hannahsten/texifyidea/util/files/PsiFile.kt @@ -6,30 +6,27 @@ import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.util.TextRange import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiDocumentManager -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiManager +import com.intellij.psi.* import com.intellij.psi.search.GlobalSearchScope -import nl.hannahsten.texifyidea.file.BibtexFileType -import nl.hannahsten.texifyidea.file.ClassFileType -import nl.hannahsten.texifyidea.file.LatexFileType -import nl.hannahsten.texifyidea.file.StyleFileType +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.elementType +import com.intellij.psi.util.parents +import com.intellij.refactoring.suggested.endOffset +import com.intellij.refactoring.suggested.startOffset +import nl.hannahsten.texifyidea.file.* import nl.hannahsten.texifyidea.index.LatexCommandsIndex import nl.hannahsten.texifyidea.index.LatexDefinitionIndex import nl.hannahsten.texifyidea.index.LatexEnvironmentsIndex import nl.hannahsten.texifyidea.index.LatexIncludesIndex import nl.hannahsten.texifyidea.lang.LatexPackage -import nl.hannahsten.texifyidea.psi.LatexCommands -import nl.hannahsten.texifyidea.psi.LatexEnvironment +import nl.hannahsten.texifyidea.psi.* import nl.hannahsten.texifyidea.reference.InputFileReference import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration import nl.hannahsten.texifyidea.util.* import nl.hannahsten.texifyidea.util.magic.FileMagic -import nl.hannahsten.texifyidea.util.parser.allCommands -import nl.hannahsten.texifyidea.util.parser.getIncludedFiles -import nl.hannahsten.texifyidea.util.parser.isDefinition -import nl.hannahsten.texifyidea.util.parser.requiredParameter +import nl.hannahsten.texifyidea.util.parser.* /** * Get the file search scope for this psi file. @@ -268,4 +265,50 @@ fun PsiFile.getBibtexRunConfigurations() = project .filter { it.mainFile == findRootFile().virtualFile } .flatMap { it.bibRunConfigs } .map { it.configuration } - .filterIsInstance() \ No newline at end of file + .filterIsInstance() + +/** + * Gets the smallest extractable expression at the given offset. this should reaaaly be a LatexFile + */ +fun PsiFile.expressionAtOffset(offset: Int): PsiElement? { + val element = findElementAt(offset) ?: return null + + return element.parents(true) + .firstOrNull { it.elementType == LatexTypes.NORMAL_TEXT_WORD || it is LatexNormalText || it is LatexParameter || it is LatexMathContent || it is LatexCommandWithParams } +} + +// should the reciever just be `LatexFile`? +fun PsiFile.findExpressionInRange(startOffset: Int, endOffset: Int): LatexExtractablePSI? { + val firstUnresolved = findElementAt(startOffset) ?: return null + val first = + if (firstUnresolved is PsiWhiteSpace) + findElementAt(firstUnresolved.endOffset) ?: return null + else + firstUnresolved + + val lastUnresolved = findElementAt(endOffset - 1) ?: return null + val last = + if (lastUnresolved is PsiWhiteSpace) + findElementAt(lastUnresolved.startOffset - 1) ?: return null + else + lastUnresolved + + val parent = PsiTreeUtil.findCommonParent(first, last) ?: return null + + return if (parent is LatexNormalText) { + parent.asExtractable(TextRange(startOffset - parent.startOffset, endOffset - parent.startOffset)) + } + else + parent.asExtractable() +} + +fun PsiFile.findExpressionAtCaret(offset: Int): PsiElement? { + val expr = expressionAtOffset(offset) + val exprBefore = expressionAtOffset(offset - 1) + return when { + expr == null -> exprBefore + exprBefore == null -> expr + PsiTreeUtil.isAncestor(expr, exprBefore, false) -> exprBefore + else -> expr + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/util/parser/LatexExtractablePSI.kt b/src/nl/hannahsten/texifyidea/util/parser/LatexExtractablePSI.kt new file mode 100644 index 000000000..f8136411f --- /dev/null +++ b/src/nl/hannahsten/texifyidea/util/parser/LatexExtractablePSI.kt @@ -0,0 +1,33 @@ +package nl.hannahsten.texifyidea.util.parser + +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.refactoring.suggested.startOffset +import nl.hannahsten.texifyidea.util.toIntRange + +/** + * A wrapper for [PsiElement] that includes a range to extract. This allows extracting subtext from a text block or other non-total extracitons + */ +class LatexExtractablePSI( + val self: PsiElement, + /** + * The range of text to extract relative to the start of my text + */ + val extractableRange: TextRange = TextRange(0, self.textLength) +) : PsiElement by self { + /** + * The range of text to extract relative to the file's start. + */ + val extractableRangeInFile + get() = TextRange( + startOffset + extractableRange.startOffset, + startOffset + extractableRange.endOffset + ) + + val extractableIntRange + get() = extractableRange.toIntRange() +} + +fun PsiElement.asExtractable(): LatexExtractablePSI = LatexExtractablePSI(this) + +fun PsiElement.asExtractable(range: TextRange): LatexExtractablePSI = LatexExtractablePSI(this, range) \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/util/parser/LatexPsiUtil.kt b/src/nl/hannahsten/texifyidea/util/parser/LatexPsiUtil.kt index c5cc9e8e0..c342353a0 100644 --- a/src/nl/hannahsten/texifyidea/util/parser/LatexPsiUtil.kt +++ b/src/nl/hannahsten/texifyidea/util/parser/LatexPsiUtil.kt @@ -1,7 +1,10 @@ package nl.hannahsten.texifyidea.util.parser +import com.intellij.codeInsight.PsiEquivalenceUtil import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.PsiRecursiveElementVisitor +import nl.hannahsten.texifyidea.file.LatexFile import nl.hannahsten.texifyidea.lang.Environment import nl.hannahsten.texifyidea.psi.* import nl.hannahsten.texifyidea.util.files.commandsInFileSet @@ -130,6 +133,27 @@ val LatexParameterText.command: PsiElement? return this.firstParentOfType(LatexCommands::class)?.firstChild } -// fun LatexOptionalKeyValPair.getKeyValValue() { -// PsiTreeUtil.getChildOfType(this, LatexKeyValValue::class.java) -// } \ No newline at end of file +/** + * Finds occurrences in the sub scope of expr, so that all will be replaced if replace all is selected. + */ +fun PsiElement.findOccurrences(): List { + val parent = firstParentOfType(LatexFile::class) + ?: return emptyList() + return this.findOccurrences(parent) +} + +fun PsiElement.findOccurrences(searchRoot: PsiElement): List { + val visitor = object : PsiRecursiveElementVisitor() { + val foundOccurrences = ArrayList() + override fun visitElement(element: PsiElement) { + if (PsiEquivalenceUtil.areElementsEquivalent(this@findOccurrences, element)) { + foundOccurrences.add(element) + } + else { + super.visitElement(element) + } + } + } + searchRoot.acceptChildren(visitor) + return visitor.foundOccurrences.map { it.asExtractable() } +} \ No newline at end of file diff --git a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt index 903791413..acd7da70b 100644 --- a/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt +++ b/test/nl/hannahsten/texifyidea/refactoring/IntroduceVariableTest.kt @@ -2,10 +2,9 @@ package nl.hannahsten.texifyidea.refactoring import com.intellij.testFramework.fixtures.BasePlatformTestCase import nl.hannahsten.texifyidea.file.LatexFileType -import nl.hannahsten.texifyidea.refactoring.introduceCommand.ExtractExpressionUi -import nl.hannahsten.texifyidea.refactoring.introduceCommand.LatexExtractablePSI -import nl.hannahsten.texifyidea.refactoring.introduceCommand.withMockTargetExpressionChooser -import nl.hannahsten.texifyidea.util.toIntRange +import nl.hannahsten.texifyidea.refactoring.introducecommand.ExtractExpressionUi +import nl.hannahsten.texifyidea.util.parser.LatexExtractablePSI +import nl.hannahsten.texifyidea.refactoring.introducecommand.withMockTargetExpressionChooser class IntroduceVariableTest : BasePlatformTestCase() { fun testBasicCaret() = doTest( @@ -190,11 +189,11 @@ class IntroduceVariableTest : BasePlatformTestCase() { override fun chooseTarget(exprs: List): LatexExtractablePSI { shownTargetChooser = true println("saw") - exprs.forEach { println("'" + it.text.substring(it.extractableRange.toIntRange()) + "'") } + exprs.forEach { println("'" + it.text.substring(it.extractableIntRange) + "'") } println("xpect") expressions.map { println("'" + it + "'") } assertEquals( - exprs.map { it.text.substring(it.extractableRange.toIntRange()).trimIndent() }, + exprs.map { it.text.substring(it.extractableIntRange).trimIndent() }, expressions.map { it.trimIndent() } ) return exprs[target] From 434909a1954d0686598b490a6d240825f887f54c Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Sun, 1 Oct 2023 11:21:20 -0600 Subject: [PATCH 26/31] Reduce complexity, increase performance --- .../LatexExtractCommandHandler.kt | 91 +++++++------------ 1 file changed, 31 insertions(+), 60 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt index 4ccb58beb..250cf02c4 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt @@ -112,76 +112,47 @@ private class ExpressionReplacer( commandName: String ) { val containingFile = chosenExpr.containingFile - if (chosenExpr.elementType == NORMAL_TEXT_WORD || chosenExpr.self is LatexNormalText) { - runWriteCommandAction(project, commandName) { - val letBinding = insertCommandDefinition( - containingFile, - chosenExpr.text.substring(chosenExpr.extractableIntRange) - ) - ?: return@runWriteCommandAction - exprs.filter { it != chosenExpr }.forEach { - val newItem = it.text.replace( - chosenExpr.text.substring(chosenExpr.extractableIntRange), - "\\mycommand" - ) - it.replace(psiFactory.createFromText(newItem).firstChild) - } - val newItem = chosenExpr.text.replace( + runWriteCommandAction(project, commandName) { + + val letBinding = insertCommandDefinition( + containingFile, + chosenExpr.text.substring(chosenExpr.extractableIntRange) + ) + ?: return@runWriteCommandAction + exprs.filter { it != chosenExpr }.forEach { + val newItem = it.text.replace( chosenExpr.text.substring(chosenExpr.extractableIntRange), "\\mycommand" ) - chosenExpr.replace(psiFactory.createFromText(newItem).firstChild) - - val letOffset = letBinding.textRange - - PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) - - println("you have beautiful eyes") - - val respawnedLetBinding = (containingFile as LatexFile).findExpressionAtCaret(letOffset.startOffset) - ?: throw IllegalStateException("This really sux") + it.replace(psiFactory.createFromText(newItem).firstChild) + } + val newItem = chosenExpr.text.replace( + chosenExpr.text.substring(chosenExpr.extractableIntRange), + "\\mycommand" + ) + chosenExpr.replace(psiFactory.createFromText(newItem).firstChild) - val filterIsInstance = - respawnedLetBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() - val actualToken = - filterIsInstance.firstOrNull { it.text == "\\mycommand" } - ?: throw IllegalStateException("How did this happen??") + val letOffset = letBinding.textRange - editor.caretModel.moveToOffset(actualToken.textRange.startOffset) + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) - LatexInPlaceVariableIntroducer( - actualToken, editor, project, "choose a variable" - ) - .performInplaceRefactoring(LinkedHashSet()) - } - } - else { - runWriteCommandAction(project, commandName) { - val name = psiFactory.createFromText("\\mycommand").firstChildOfType(LatexCommands::class) - ?: return@runWriteCommandAction + println("you have beautiful eyes") - val letBinding = insertCommandDefinition( - containingFile, - chosenExpr.text.substring(chosenExpr.extractableIntRange) - ) - ?: return@runWriteCommandAction - exprs.filter { it != chosenExpr }.forEach { it.replace(name) } - chosenExpr.replace(name) as LatexCommands + val respawnedLetBinding = (containingFile as LatexFile).findExpressionAtCaret(letOffset.startOffset) + ?: throw IllegalStateException("This really sux") - PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) - val filterIsInstance = - letBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() - val actualToken = - filterIsInstance.firstOrNull { it.text == "\\mycommand" } - ?: throw IllegalStateException("How did this happen??") + val filterIsInstance = + respawnedLetBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() + val actualToken = + filterIsInstance.firstOrNull { it.text == "\\mycommand" } + ?: throw IllegalStateException("How did this happen??") - editor.caretModel.moveToOffset(actualToken.textRange.startOffset) + editor.caretModel.moveToOffset(actualToken.textRange.startOffset) - LatexInPlaceVariableIntroducer( - actualToken, editor, project, "choose a variable" - ) - .performInplaceRefactoring(LinkedHashSet()) - } + LatexInPlaceVariableIntroducer( + actualToken, editor, project, "choose a variable" + ) + .performInplaceRefactoring(LinkedHashSet()) } } } From caf0ed5d8e5a0975fa295c0c80b4fb0930e06caf Mon Sep 17 00:00:00 2001 From: jojo2357 <66704796+jojo2357@users.noreply.github.com> Date: Mon, 16 Oct 2023 22:42:06 -0600 Subject: [PATCH 27/31] Clean Up --- .../LatexExtractCommandHandler.kt | 51 +++++++++++++------ .../texifyidea/util/files/PsiFile.kt | 22 +++++--- .../texifyidea/util/parser/LatexPsiUtil.kt | 6 ++- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt index 250cf02c4..a6b39df0b 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt @@ -37,6 +37,7 @@ class LatexExtractCommandHandler : RefactoringActionHandler { if (file !is LatexFile) return val exprs = findCandidateExpressionsToExtract(editor, file) + // almost never happens, so the error will be likely worded wrong, but hopefully that will generate more bug reports! if (exprs.isEmpty()) { val message = RefactoringBundle.message( if (editor.selectionModel.hasSelection()) @@ -57,6 +58,7 @@ class LatexExtractCommandHandler : RefactoringActionHandler { if (exprs.size == 1) { extractor(exprs.single()) } + // if there are multiple candidates (ie the user did not have an active selection, ask for them to choose what to extract else showExpressionChooser(editor, exprs) { extractor(it) } @@ -64,22 +66,22 @@ class LatexExtractCommandHandler : RefactoringActionHandler { } override fun invoke(project: Project, elements: Array, dataContext: DataContext?) { - TODO("This was not meant to happen like this") + TODO("This should never get called") } } fun showExpressionChooser( editor: Editor, - exprs: List, + candidates: List, callback: (LatexExtractablePSI) -> Unit ) { if (isUnitTestMode) { - callback(MOCK!!.chooseTarget(exprs)) + callback(MOCK!!.chooseTarget(candidates)) } else IntroduceTargetChooser.showChooser( editor, - exprs, + candidates, callback.asPass, { it.text.substring(it.extractableIntRange) }, RefactoringBundle.message("introduce.target.chooser.expressions.title"), @@ -107,14 +109,17 @@ private class ExpressionReplacer( ) { private val psiFactory = LatexPsiHelper(project) + /** + * This actually replaces all the ocurrences + */ fun replaceElementForAllExpr( exprs: List, commandName: String ) { + // cache file in case the psi tree breaks val containingFile = chosenExpr.containingFile runWriteCommandAction(project, commandName) { - - val letBinding = insertCommandDefinition( + val definitionToken = insertCommandDefinition( containingFile, chosenExpr.text.substring(chosenExpr.extractableIntRange) ) @@ -132,23 +137,24 @@ private class ExpressionReplacer( ) chosenExpr.replace(psiFactory.createFromText(newItem).firstChild) - val letOffset = letBinding.textRange + val definitionOffset = definitionToken.textRange PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) - println("you have beautiful eyes") - - val respawnedLetBinding = (containingFile as LatexFile).findExpressionAtCaret(letOffset.startOffset) - ?: throw IllegalStateException("This really sux") + // sometimes calling the previous line will invalidate `definitionToken`, so we will make sure to find the actual valid token + val vampireCommandDefinition = containingFile.findExpressionAtCaret(definitionOffset.startOffset) + ?: throw IllegalStateException("Unexpectedly could not find an expression") - val filterIsInstance = - respawnedLetBinding.childrenOfType(PsiNamedElement::class).filterIsInstance() val actualToken = - filterIsInstance.firstOrNull { it.text == "\\mycommand" } - ?: throw IllegalStateException("How did this happen??") + vampireCommandDefinition + .childrenOfType(PsiNamedElement::class) + .filterIsInstance() + .firstOrNull { it.text == "\\mycommand" } + ?: throw IllegalStateException("Psi Tree was not in the expected state") editor.caretModel.moveToOffset(actualToken.textRange.startOffset) + // unsure where title is used. Either way, put the user into a refactor where they get to specify the new command name LatexInPlaceVariableIntroducer( actualToken, editor, project, "choose a variable" ) @@ -185,8 +191,12 @@ private val ((T) -> Unit).asPass: Pass override fun pass(t: T) = this@asPass(t) } +/** + * Returns a list of "expressions" which could be extracted. + */ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { val selection = editor.selectionModel + // if the user has highlighted a block, simply return that if (selection.hasSelection()) { // If there's an explicit selection, suggest only one expression return listOfNotNull(file.findExpressionInRange(selection.selectionStart, selection.selectionEnd)) @@ -194,6 +204,7 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List): List } +// This allows us to run tests and mimic user input var MOCK: ExtractExpressionUi? = null @TestOnly diff --git a/src/nl/hannahsten/texifyidea/util/files/PsiFile.kt b/src/nl/hannahsten/texifyidea/util/files/PsiFile.kt index e830d6f83..6cc0aa1a8 100644 --- a/src/nl/hannahsten/texifyidea/util/files/PsiFile.kt +++ b/src/nl/hannahsten/texifyidea/util/files/PsiFile.kt @@ -268,7 +268,7 @@ fun PsiFile.getBibtexRunConfigurations() = project .filterIsInstance() /** - * Gets the smallest extractable expression at the given offset. this should reaaaly be a LatexFile + * Gets the smallest extractable expression at the given offset */ fun PsiFile.expressionAtOffset(offset: Int): PsiElement? { val element = findElementAt(offset) ?: return null @@ -277,31 +277,37 @@ fun PsiFile.expressionAtOffset(offset: Int): PsiElement? { .firstOrNull { it.elementType == LatexTypes.NORMAL_TEXT_WORD || it is LatexNormalText || it is LatexParameter || it is LatexMathContent || it is LatexCommandWithParams } } -// should the reciever just be `LatexFile`? +/** + * Get "expression" within range specified. An expression is either a PsiElement, or a PsiElement with a specific extraction range in the case that the range lies entirely within a text block + */ fun PsiFile.findExpressionInRange(startOffset: Int, endOffset: Int): LatexExtractablePSI? { val firstUnresolved = findElementAt(startOffset) ?: return null - val first = + val startElement = if (firstUnresolved is PsiWhiteSpace) findElementAt(firstUnresolved.endOffset) ?: return null else firstUnresolved val lastUnresolved = findElementAt(endOffset - 1) ?: return null - val last = + val endElement = if (lastUnresolved is PsiWhiteSpace) findElementAt(lastUnresolved.startOffset - 1) ?: return null else lastUnresolved - val parent = PsiTreeUtil.findCommonParent(first, last) ?: return null + val commonParent = PsiTreeUtil.findCommonParent(startElement, endElement) ?: return null - return if (parent is LatexNormalText) { - parent.asExtractable(TextRange(startOffset - parent.startOffset, endOffset - parent.startOffset)) + // We will consider an exression to be a sentence or a substring out of text. Here we will mark that in the extraction range. + return if (commonParent is LatexNormalText) { + commonParent.asExtractable(TextRange(startOffset - commonParent.startOffset, endOffset - commonParent.startOffset)) } else - parent.asExtractable() + commonParent.asExtractable() } +/** + * Attempts to find the "expression" at the given offset + */ fun PsiFile.findExpressionAtCaret(offset: Int): PsiElement? { val expr = expressionAtOffset(offset) val exprBefore = expressionAtOffset(offset - 1) diff --git a/src/nl/hannahsten/texifyidea/util/parser/LatexPsiUtil.kt b/src/nl/hannahsten/texifyidea/util/parser/LatexPsiUtil.kt index c342353a0..49d7028cc 100644 --- a/src/nl/hannahsten/texifyidea/util/parser/LatexPsiUtil.kt +++ b/src/nl/hannahsten/texifyidea/util/parser/LatexPsiUtil.kt @@ -134,7 +134,7 @@ val LatexParameterText.command: PsiElement? } /** - * Finds occurrences in the sub scope of expr, so that all will be replaced if replace all is selected. + * @see PsiElement.findOccurrences(PsiElement) */ fun PsiElement.findOccurrences(): List { val parent = firstParentOfType(LatexFile::class) @@ -142,6 +142,10 @@ fun PsiElement.findOccurrences(): List { return this.findOccurrences(parent) } +/** + * Known weakness: since we will allow the user to extract portions of a text block, this will only extract text when the parent PSI's are identical. + * However, extracting a single word does not suffer from this as we are extracting an actual token. + */ fun PsiElement.findOccurrences(searchRoot: PsiElement): List { val visitor = object : PsiRecursiveElementVisitor() { val foundOccurrences = ArrayList() From a4dade0ca1d13728a4d2192311f0257c27662165 Mon Sep 17 00:00:00 2001 From: Thomas Schouten Date: Fri, 24 Nov 2023 09:48:40 +0100 Subject: [PATCH 28/31] Reduce code duplication --- src/nl/hannahsten/texifyidea/util/Commands.kt | 54 ++--------- src/nl/hannahsten/texifyidea/util/Packages.kt | 89 ++++++++++++------- 2 files changed, 64 insertions(+), 79 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/util/Commands.kt b/src/nl/hannahsten/texifyidea/util/Commands.kt index 8dcd52548..03e57d51d 100644 --- a/src/nl/hannahsten/texifyidea/util/Commands.kt +++ b/src/nl/hannahsten/texifyidea/util/Commands.kt @@ -1,7 +1,6 @@ package nl.hannahsten.texifyidea.util 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.search.GlobalSearchScope @@ -13,6 +12,7 @@ import nl.hannahsten.texifyidea.lang.commands.RequiredFileArgument import nl.hannahsten.texifyidea.psi.LatexCommands import nl.hannahsten.texifyidea.psi.LatexParameter import nl.hannahsten.texifyidea.psi.LatexPsiHelper +import nl.hannahsten.texifyidea.util.PackageUtils.getDefaultInsertAnchor import nl.hannahsten.texifyidea.util.files.* import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands import nl.hannahsten.texifyidea.util.magic.CommandMagic @@ -70,30 +70,6 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: } } - // The anchor after which the new element will be inserted - val anchorAfter: PsiElement? - - // When there are no usepackage commands: insert below documentclass. - if (last == null) { - val classHuh = commands.asSequence() - .filter { cmd -> - "\\documentclass" == cmd.commandToken - .text || "\\LoadClass" == cmd.commandToken.text - } - .firstOrNull() - if (classHuh != null) { - anchorAfter = classHuh - } - else { - // No other sensible location can be found - anchorAfter = null - } - } - // Otherwise, insert below the lowest usepackage. - else { - anchorAfter = last - } - val blockingNames = file.definitions().filter { it.commandToken.text.matches("${newCommandName}\\d*".toRegex()) } val nonConflictingName = "${newCommandName}${if (blockingNames.isEmpty()) "" else blockingNames.size.toString()}" @@ -102,29 +78,11 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: val newChild = LatexPsiHelper(file.project).createFromText(command).firstChild val newNode = newChild.node - // Don't run in a write action, as that will produce a SideEffectsNotAllowedException for INVOKE_LATER - - // Avoid "Attempt to modify PSI for non-committed Document" - // https://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/modifying_psi.html?search=refac#combining-psi-and-document-modifications - PsiDocumentManager.getInstance(file.project) - .doPostponedOperationsAndUnblockDocument(file.document() ?: return null) - PsiDocumentManager.getInstance(file.project).commitDocument(file.document() ?: return null) - - runWriteAction { - val newLine = LatexPsiHelper(file.project).createFromText("\n\n").firstChild.node - // Avoid NPE, see #3083 (cause unknown) - if (anchorAfter != null && com.intellij.psi.impl.source.tree.TreeUtil.getFileElement(anchorAfter.parent.node) != null) { - val anchorBefore = anchorAfter.node.treeNext - anchorAfter.parent.node.addChild(newLine, anchorBefore) - anchorAfter.parent.node.addChild(newNode, anchorBefore) - } - else { - // Insert at beginning - file.node.addChild(newLine, file.firstChild.node) - file.node.addChild(newNode, file.firstChild.node) -// file.node.addChild(newLine, file.firstChild.node) - } - } + // The anchor after which the new element will be inserted + // When there are no usepackage commands: insert below documentclass. + val (anchorAfter, _) = getDefaultInsertAnchor(commands, last) + + PackageUtils.insertNodeAfterAnchor(file, anchorAfter, prependNewLine = true, newNode, prependBlankLine = true) return newChild } diff --git a/src/nl/hannahsten/texifyidea/util/Packages.kt b/src/nl/hannahsten/texifyidea/util/Packages.kt index 5bef1065a..764e77f09 100644 --- a/src/nl/hannahsten/texifyidea/util/Packages.kt +++ b/src/nl/hannahsten/texifyidea/util/Packages.kt @@ -1,10 +1,12 @@ package nl.hannahsten.texifyidea.util +import com.intellij.lang.ASTNode import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.impl.source.tree.TreeUtil import nl.hannahsten.texifyidea.index.file.LatexExternalPackageInclusionCache import nl.hannahsten.texifyidea.lang.LatexPackage import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand @@ -14,6 +16,7 @@ import nl.hannahsten.texifyidea.settings.TexifySettings import nl.hannahsten.texifyidea.util.files.* import nl.hannahsten.texifyidea.util.magic.CommandMagic import nl.hannahsten.texifyidea.util.magic.PackageMagic +import nl.hannahsten.texifyidea.util.magic.cmd import nl.hannahsten.texifyidea.util.parser.firstParentOfType import nl.hannahsten.texifyidea.util.parser.toStringMap @@ -34,6 +37,38 @@ object PackageUtils { ?.split(";") ?.toList() ?: emptyList() + /** + * Get the default psi element to insert new packages/definitions after. + * The anchor will be the given preferred anchor if not null. + */ + fun getDefaultInsertAnchor(commands: Collection, preferredAnchor: LatexCommands?): Pair { + val classHuh = commands.asSequence() + .filter { cmd -> + cmd.name == LatexGenericRegularCommand.DOCUMENTCLASS.cmd || cmd.name == LatexGenericRegularCommand.LOADCLASS.cmd + } + .firstOrNull() + val anchorAfter: PsiElement? + val prependNewLine: Boolean + if (classHuh != null) { + anchorAfter = classHuh + prependNewLine = true + } + else { + // No other sensible location can be found + anchorAfter = null + prependNewLine = false + } + + return if (preferredAnchor == null) { + Pair(anchorAfter, prependNewLine) + } + else { + Pair(preferredAnchor, true) + } + } + + + /** * Inserts a usepackage statement for the given package in a certain file. * @@ -69,33 +104,7 @@ object PackageUtils { } } - val prependNewLine: Boolean - // The anchor after which the new element will be inserted - val anchorAfter: PsiElement? - - // When there are no usepackage commands: insert below documentclass. - if (last == null) { - val classHuh = commands.asSequence() - .filter { cmd -> - "\\documentclass" == cmd.commandToken - .text || "\\LoadClass" == cmd.commandToken.text - } - .firstOrNull() - if (classHuh != null) { - anchorAfter = classHuh - prependNewLine = true - } - else { - // No other sensible location can be found - anchorAfter = null - prependNewLine = false - } - } - // Otherwise, insert below the lowest usepackage. - else { - anchorAfter = last - prependNewLine = true - } + val (anchorAfter, prependNewLine) = getDefaultInsertAnchor(commands, last) var command = commandName command += if (parameters == null || "" == parameters) "" else "[$parameters]" @@ -103,6 +112,21 @@ object PackageUtils { val newNode = LatexPsiHelper(file.project).createFromText(command).firstChild.node + insertNodeAfterAnchor(file, anchorAfter, prependNewLine, newNode) + } + + /** + * Insert an AST node after a certain anchor, possibly with a newline. + * + * @param prependBlankLine If prependNewLine is true, you can set this to true to insert an additional blank line. + */ + fun insertNodeAfterAnchor( + file: PsiFile, + anchorAfter: PsiElement?, + prependNewLine: Boolean, + newNode: ASTNode, + prependBlankLine: Boolean = false + ) { // Don't run in a write action, as that will produce a SideEffectsNotAllowedException for INVOKE_LATER // Avoid "Attempt to modify PSI for non-committed Document" @@ -111,18 +135,21 @@ object PackageUtils { .doPostponedOperationsAndUnblockDocument(file.document() ?: return) PsiDocumentManager.getInstance(file.project).commitDocument(file.document() ?: return) runWriteAction { + val newlineText = if (prependBlankLine) "\n\n" else "\n" + val newLine = LatexPsiHelper(file.project).createFromText(newlineText).firstChild.node // Avoid NPE, see #3083 (cause unknown) - if (anchorAfter != null && com.intellij.psi.impl.source.tree.TreeUtil.getFileElement(anchorAfter.parent.node) != null) { + if (anchorAfter != null && TreeUtil.getFileElement(anchorAfter.parent.node) != null) { val anchorBefore = anchorAfter.node.treeNext - @Suppress("KotlinConstantConditions") - if (prependNewLine) { - val newLine = LatexPsiHelper(file.project).createFromText("\n").firstChild.node + if (prependNewLine || prependBlankLine) { anchorAfter.parent.node.addChild(newLine, anchorBefore) } anchorAfter.parent.node.addChild(newNode, anchorBefore) } else { // Insert at beginning + if (prependNewLine || prependBlankLine) { + file.node.addChild(newLine, file.firstChild.node) + } file.node.addChild(newNode, file.firstChild.node) } } From a7bd6fd090bacfe6caed563cc6d6a9099052152c Mon Sep 17 00:00:00 2001 From: Thomas Schouten Date: Fri, 24 Nov 2023 10:20:01 +0100 Subject: [PATCH 29/31] Remove hostility and clean up --- .../LatexExtractCommandHandler.kt | 13 ++++++----- src/nl/hannahsten/texifyidea/util/Commands.kt | 23 +++++++------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt index a6b39df0b..41abc3ec6 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt @@ -6,7 +6,10 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.util.Pass import com.intellij.openapi.util.TextRange -import com.intellij.psi.* +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiNamedElement import com.intellij.psi.util.PsiTreeUtil.findCommonParent import com.intellij.psi.util.elementType import com.intellij.psi.util.parents @@ -45,7 +48,7 @@ class LatexExtractCommandHandler : RefactoringActionHandler { else "refactoring.introduce.selection.error" ) - val title = RefactoringBundle.message("introduce.variable.title") + val title = "Introduce Custom Command" val helpId = "refactoring.extractVariable" CommonRefactoringUtil.showErrorHint(project, editor, message, title, helpId) } @@ -65,9 +68,7 @@ class LatexExtractCommandHandler : RefactoringActionHandler { } } - override fun invoke(project: Project, elements: Array, dataContext: DataContext?) { - TODO("This should never get called") - } + override fun invoke(project: Project, elements: Array, dataContext: DataContext?) { } } fun showExpressionChooser( @@ -229,7 +230,7 @@ fun findCandidateExpressionsToExtract(editor: Editor, file: LatexFile): List { } /** - * Inserts a usepackage statement for the given package in a certain file. - * - * @param file - * The file to add the usepackage statement to. - * @param packageName - * The name of the package to insert. - * @param parameters - * Parameters to add to the statement, `null` or empty string for no parameters. + * Inserts a custom c custom command definition. */ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: String = "mycommand"): PsiElement? { if (!file.isWritable) return null @@ -58,13 +51,13 @@ fun insertCommandDefinition(file: PsiFile, commandText: String, newCommandName: var last: LatexCommands? = null for (cmd in commands) { - if (cmd.commandToken.text == "\\newcommand") { + if (cmd.name == LatexNewDefinitionCommand.NEWCOMMAND.cmd) { last = cmd } - else if (cmd.commandToken.text == "\\usepackage") { + else if (cmd.name == LatexGenericRegularCommand.USEPACKAGE.cmd) { last = cmd } - else if (cmd.commandToken.text == "\\begin" && cmd.requiredParameter(0) == "document") { + else if (cmd.name == LatexGenericRegularCommand.BEGIN.cmd && cmd.requiredParameter(0) == "document") { last = cmd break } From a25b6f5b88ba2782b47ccd59e429a3879a39588a Mon Sep 17 00:00:00 2001 From: Thomas Schouten Date: Fri, 24 Nov 2023 10:21:11 +0100 Subject: [PATCH 30/31] Capitalization --- .../refactoring/introducecommand/LatexExtractCommandHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt index 41abc3ec6..c310fd4d7 100644 --- a/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt +++ b/src/nl/hannahsten/texifyidea/refactoring/introducecommand/LatexExtractCommandHandler.kt @@ -157,7 +157,7 @@ private class ExpressionReplacer( // unsure where title is used. Either way, put the user into a refactor where they get to specify the new command name LatexInPlaceVariableIntroducer( - actualToken, editor, project, "choose a variable" + actualToken, editor, project, "Choose a Variable" ) .performInplaceRefactoring(LinkedHashSet()) } From 18ea366ae79f630d703027ee8fc26cac83ca70c0 Mon Sep 17 00:00:00 2001 From: Thomas Schouten Date: Fri, 24 Nov 2023 10:22:52 +0100 Subject: [PATCH 31/31] Formatting --- src/nl/hannahsten/texifyidea/util/Packages.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/util/Packages.kt b/src/nl/hannahsten/texifyidea/util/Packages.kt index 764e77f09..dc6a9919c 100644 --- a/src/nl/hannahsten/texifyidea/util/Packages.kt +++ b/src/nl/hannahsten/texifyidea/util/Packages.kt @@ -67,8 +67,6 @@ object PackageUtils { } } - - /** * Inserts a usepackage statement for the given package in a certain file. *