Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Allow quick label change #1733

Merged
merged 9 commits into from
Jan 12, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -719,26 +719,36 @@
<localInspection language="Bibtex" implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexDuplicateIdInspection"
groupName="BibTeX" displayName="Duplicate ID"
enabledByDefault="true"/>
<localInspection language="Latex" implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexMissingBibliographystyleInspection"
<localInspection language="Latex"
implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexMissingBibliographystyleInspection"
groupName="BibTeX" displayName="Missing bibliography style"
enabledByDefault="true"/>
<localInspection language="Latex" implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexDuplicateBibliographystyleInspection"
<localInspection language="Latex"
implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexDuplicateBibliographystyleInspection"
groupName="BibTeX" displayName="Duplicate bibliography style commands"
enabledByDefault="true"/>
<localInspection language="Latex" implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexDuplicateBibliographyInspection"
<localInspection language="Latex"
implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexDuplicateBibliographyInspection"
groupName="BibTeX" displayName="Same bibliography is included multiple times"
enabledByDefault="true"/>
<localInspection language="Bibtex" implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexUnusedEntryInspection"
<localInspection language="Bibtex"
implementationClass="nl.hannahsten.texifyidea.inspections.bibtex.BibtexUnusedEntryInspection"
groupName="BibTeX" displayName="Unused bib entry"
enabledByDefault="true"/>

<!-- Intentions -->
<intentionAction>
<className>nl.hannahsten.texifyidea.intentions.LatexAddLabelIntention</className>
<className>nl.hannahsten.texifyidea.intentions.LatexAddLabelToCommandIntention</className>
<category>LaTeX</category>
<descriptionDirectoryName>LatexAddLabelIntention</descriptionDirectoryName>
</intentionAction>

<intentionAction>
<className>nl.hannahsten.texifyidea.intentions.LatexAddLabelToEnvironmentIntention</className>
<category>LaTeX</category>
<descriptionDirectoryName>LatexAddLabelToEnvironmentIntention</descriptionDirectoryName>
</intentionAction>

<intentionAction>
<className>nl.hannahsten.texifyidea.intentions.LatexDisplayMathIntention</className>
<category>LaTeX</category>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
\begin{lstlisting}[label={lst:listing}]
\end{lstlisting}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
\begin{lstlisting}
\end{lstlisting}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html>
<body>
Add a label.
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.createSmartPointer
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.intentions.LatexAddLabelIntention
import nl.hannahsten.texifyidea.intentions.LatexAddLabelToCommandIntention
import nl.hannahsten.texifyidea.intentions.LatexAddLabelToEnvironmentIntention
import nl.hannahsten.texifyidea.lang.LatexDocumentClass
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.settings.TexifyConfigurable
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.files.*
import nl.hannahsten.texifyidea.util.hasStar
import org.jetbrains.annotations.Nls
import java.util.*

Expand Down Expand Up @@ -136,73 +136,35 @@ open class LatexMissingLabelInspection : TexifyInspectionBase() {
}
}

abstract class LabelQuickFix : LocalQuickFix {
protected fun getUniqueLabelName(base: String, prefix: String?, file: PsiFile): String {
val labelBase = "$prefix:$base"
val allLabels = file.findLatexAndBibtexLabelStringsInFileSet()
return appendCounter(labelBase, allLabels)
}

/**
* Keeps adding a counter behind the label until there is no other label with that name.
*/
private fun appendCounter(label: String, allLabels: Set<String>): String {
var counter = 2
var candidate = label

while (allLabels.contains(candidate)) {
candidate = label + (counter++)
}

return candidate
}
}

/**
* This is also an intention, but in order to keep the same alt+enter+enter functionality (because we have an other
* quickfix as well) we keep it as a quickfix also.
*/
private class InsertLabelForCommandFix : LabelQuickFix() {
private class InsertLabelForCommandFix : LocalQuickFix {

// It has to appear in alphabetical order before the other quickfix
override fun getFamilyName() = "Add label for this command"

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val command = descriptor.psiElement as LatexCommands
LatexAddLabelIntention(command.createSmartPointer()).invoke(project, command.containingFile.openedEditor(), command.containingFile)
LatexAddLabelToCommandIntention(command.createSmartPointer()).invoke(
project,
command.containingFile.openedEditor(),
command.containingFile
)
}
}

private class InsertLabelInEnvironmentFix : LabelQuickFix() {
private class InsertLabelInEnvironmentFix : LocalQuickFix {
override fun getFamilyName() = "Add label for this environment"

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val command = descriptor.psiElement as LatexEnvironment
val helper = LatexPsiHelper(project)
// Determine label name.
val createdLabel = getUniqueLabelName(
command.environmentName.formatAsLabel(),
Magic.Environment.labeled[command.environmentName], command.containingFile
val environment = descriptor.psiElement as LatexEnvironment
LatexAddLabelToEnvironmentIntention(environment.createSmartPointer()).invoke(
project,
environment.containingFile.openedEditor(),
environment.containingFile
)

val moveCaretAfter: PsiElement
moveCaretAfter = if (Magic.Environment.labelAsParameter.contains(command.environmentName)) {
helper.setOptionalParameter(command.beginCommand, "label", "{$createdLabel}")
}
else {
// in a float environment the label must be inserted after a caption
val labelCommand = helper.addToContent(
command, helper.createLabelCommand(createdLabel),
command.environmentContent?.childrenOfType<LatexCommands>()
?.findLast { c -> c.name == "\\caption" }
)
labelCommand
}

// Adjust caret offset
val openedEditor = command.containingFile.openedEditor() ?: return
val caretModel = openedEditor.caretModel
caretModel.moveToOffset(moveCaretAfter.endOffset())
}
}
}
150 changes: 71 additions & 79 deletions src/nl/hannahsten/texifyidea/intentions/LatexAddLabelIntention.kt
Original file line number Diff line number Diff line change
@@ -1,103 +1,72 @@
package nl.hannahsten.texifyidea.intentions

import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TextExpression
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import nl.hannahsten.texifyidea.psi.LatexCommands
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.util.parentOfType
import com.intellij.refactoring.rename.RenameProcessor
import com.intellij.refactoring.rename.inplace.MemberInplaceRenamer
import nl.hannahsten.texifyidea.psi.LatexCommandWithParams
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.findLatexAndBibtexLabelStringsInFileSet
import kotlin.math.max

/**
* @author Hannah Schellekens
*/
open class LatexAddLabelIntention(val command: SmartPsiElementPointer<LatexCommands>? = null) : TexifyIntentionBase("Add label") {
abstract class LatexAddLabelIntention : TexifyIntentionBase("Add label") {

private fun findCommand(editor: Editor?, file: PsiFile?): LatexCommands? {
val offset = editor?.caretModel?.offset ?: return null
val element = file?.findElementAt(offset) ?: return null
// Also check one position back, because we want it to trigger in \section{a}<caret>
return element as? LatexCommands ?: element.parentOfType(LatexCommands::class)
?: file.findElementAt(max(0, offset - 1)) as? LatexCommands
?: file.findElementAt(max(0, offset - 1))?.parentOfType(LatexCommands::class)
}

override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
if (file?.isLatexFile() == false) {
return false
/**
* This class handles the rename of a label parameter after insertion
*/
private class LabelInplaceRenamer(
elementToRename: PsiNamedElement,
editor: Editor,
private val prefix: String,
initialName: String,
private val endMarker: RangeMarker
) : MemberInplaceRenamer(elementToRename, null, editor, initialName, initialName) {

override fun getRangeToRename(element: PsiElement): TextRange {
// The label prefix is given by convention and not renamed
return TextRange(prefix.length, element.textLength)
}

return findCommand(editor, file)?.name in Magic.Command.labeledPrefixes
}

override fun startInWriteAction() = true

override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (editor == null || file == null) {
return
override fun createRenameProcessor(element: PsiElement?, newName: String?): RenameProcessor {
// Automatically prepend the prefix
return super.createRenameProcessor(element, "$prefix$newName")
}

// When no this.command is provided, use the command at the caret as the best guess.
val command: LatexCommands = this.command?.element
?: findCommand(editor, file)
?: return

// Determine label name.
val labelString: String? = if (Magic.Command.labelAsParameter.contains(command.name)) {
// For parameter labeled commands we use the command name itself
command.name!!
}
else {
// For all other commands we use the first required parameter
val required = command.requiredParameters
if (required.isNotEmpty()) {
required[0]
}
else {
null
}
override fun moveOffsetAfter(success: Boolean) {
super.moveOffsetAfter(success)
myEditor.caretModel.moveToOffset(endMarker.endOffset)
}

val prefix = Magic.Command.labeledPrefixes[command.name!!]

if (labelString != null) {
val createdLabel = getUniqueLabelName(
labelString.formatAsLabel(),
prefix, command.containingFile
)

val factory = LatexPsiHelper(project)

val labelCommand = if (Magic.Command.labelAsParameter.contains(command.name)) {
factory.setOptionalParameter(command, "label", "{$createdLabel}")
}
else {
// Insert label
// command -> NoMathContent -> Content -> Container containing the command
val commandContent = command.parent.parent
commandContent.parent.addAfter(factory.createLabelCommand(createdLabel), commandContent)
override fun restoreCaretOffsetAfterRename() {
// Dispose the marker like the parent method does, but do not move the caret. We already moved it in
// moveOffsetAfter
if (myBeforeRevert != null) {
myBeforeRevert.dispose()
}
// Adjust caret offset.
val caret = editor.caretModel
caret.moveToOffset(labelCommand.endOffset())
}
else {
editor.caretModel.moveToOffset(command.endOffset())
val template = TemplateImpl("", "\\label{$prefix:\$__Variable0\$}", "")
template.addVariable(TextExpression(""), true)
TemplateManager.getInstance(editor.project).startTemplate(editor, template)
}
}

private fun getUniqueLabelName(base: String, prefix: String?, file: PsiFile): String {
protected inline fun <reified T : PsiElement> findTarget(editor: Editor?, file: PsiFile?): T? {
val offset = editor?.caretModel?.offset ?: return null
val element = file?.findElementAt(offset) ?: return null
// Also check one position back, because we want it to trigger in \section{a}<caret>
return element as? T ?: element.parentOfType<T>()
?: file.findElementAt(max(0, offset - 1)) as? T
?: file.findElementAt(max(0, offset - 1))?.parentOfType<T>()
}

protected fun getUniqueLabelName(base: String, prefix: String, file: PsiFile): LabelWithPrefix {
val labelBase = "$prefix:$base"
val allLabels = file.findLatexAndBibtexLabelStringsInFileSet()
return appendCounter(labelBase, allLabels)
val fullLabel = appendCounter(labelBase, allLabels)
return LabelWithPrefix(prefix, fullLabel.substring(prefix.length + 1))
}

/**
Expand All @@ -113,4 +82,27 @@ open class LatexAddLabelIntention(val command: SmartPsiElementPointer<LatexComma

return candidate
}

data class LabelWithPrefix(val prefix: String, val base: String) {
val prefixText = "$prefix:"
val labelText = "$prefix:$base"
}

protected fun createLabelAndStartRename(
editor: Editor,
project: Project,
command: LatexCommandWithParams,
label: LabelWithPrefix,
moveCaretTo: RangeMarker
) {
val helper = LatexPsiHelper(project)
val parameter = helper.setOptionalParameter(command, "label", "{${label.labelText}}")
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
val parameterText =
parameter.keyvalValue!!.keyvalContentList.first().parameterGroup!!.parameterGroupText!!.parameterTextList.first()
PHPirates marked this conversation as resolved.
Show resolved Hide resolved
// Move the caret onto the label
editor.caretModel.moveToOffset(parameterText.textOffset + label.prefix.length + 1)
val renamer = LabelInplaceRenamer(parameterText, editor, label.prefixText, label.base, moveCaretTo)
renamer.performInplaceRefactoring(LinkedHashSet())
}
}
Loading