Skip to content

Commit

Permalink
Merge branch 'allow-quick-label-change'
Browse files Browse the repository at this point in the history
  • Loading branch information
PHPirates committed Jan 12, 2021
2 parents 868ed22 + ce6f206 commit 17cb4ec
Show file tree
Hide file tree
Showing 15 changed files with 368 additions and 163 deletions.
20 changes: 15 additions & 5 deletions resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -726,26 +726,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())
}
}
}
155 changes: 76 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!!
override fun moveOffsetAfter(success: Boolean) {
super.moveOffsetAfter(success)
myEditor.caretModel.moveToOffset(endMarker.endOffset)
}
else {
// For all other commands we use the first required parameter
val required = command.requiredParameters
if (required.isNotEmpty()) {
required[0]
}
else {
null
}
}

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}")
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()
}
else {
// Insert label
// command -> NoMathContent -> Content -> Container containing the command
val commandContent = command.parent.parent
commandContent.parent.addAfter(factory.createLabelCommand(createdLabel), commandContent)
}
// 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,32 @@ 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)

// setOptionalParameter should create an appropriate optionaArgument node with label={text} in it
val parameterText =
parameter.keyvalValue?.keyvalContentList?.firstOrNull()?.parameterGroup?.parameterGroupText?.parameterTextList?.firstOrNull()
?: throw AssertionError("parameter created by setOptionalParameter does not have the right structure")
// 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())
}

override fun startInWriteAction() = true
}
Loading

0 comments on commit 17cb4ec

Please sign in to comment.