diff --git a/CHANGELOG.md b/CHANGELOG.md index 105ed6434..866e8dd62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Added +* Support label references to user defined listings environment * Add option to disable automatic compilation in power save mode * Convert automatic compilation settings to a combobox * Add checkboxes to graphic insertion wizard for relative width or height diff --git a/Writerside/topics/Code-navigation.md b/Writerside/topics/Code-navigation.md index 13860b36e..859940c0c 100644 --- a/Writerside/topics/Code-navigation.md +++ b/Writerside/topics/Code-navigation.md @@ -26,7 +26,8 @@ Web links in `\url` and `\href` commands are clickable using Ctrl + Cl By pressing Ctrl + B on a reference to a label, or a citation of a bibtex entry, your cursor will go to the declaration of the reference or citation. In general, you can go back to your previous cursor location with Ctrl + Alt + <- -This also works with usages of commands defined with `\newcommand` definitions (in your fileset, not in LaTeX packages), but only if your command definition includes braces, like `\newcommand{\mycommand}{definition}` +This also works with usages of commands defined with `\newcommand` definitions (in your fileset, not in LaTeX packages), but only if your command definition includes braces, like `\newcommand{\mycommand}{definition}`. +It also works for user defined environments that accept a label as parameter, for example using `\lstnewenvironment`. ![go-to-label-declaration](go-to-label-declaration.gif) diff --git a/src/nl/hannahsten/texifyidea/lang/LabelingEnvironmentInformation.kt b/src/nl/hannahsten/texifyidea/lang/LabelingEnvironmentInformation.kt new file mode 100644 index 000000000..9f9ac8527 --- /dev/null +++ b/src/nl/hannahsten/texifyidea/lang/LabelingEnvironmentInformation.kt @@ -0,0 +1,13 @@ +package nl.hannahsten.texifyidea.lang + +import arrow.core.NonEmptyList + +/** + * Information about a user-defined environment which has a \label command in the definition. + */ +data class LabelingEnvironmentInformation( + /** Parameter positions which define a label, starting from 0 (note: LaTeX starts from 1). */ + var positions: NonEmptyList, + /** Default label prefix, for example in \newcommand{\mylabel}[1]{\label{sec:#1}} it would be sec: */ + var prefix: String = "" +) diff --git a/src/nl/hannahsten/texifyidea/lang/alias/EnvironmentManager.kt b/src/nl/hannahsten/texifyidea/lang/alias/EnvironmentManager.kt index 58f525981..1a27d5f51 100644 --- a/src/nl/hannahsten/texifyidea/lang/alias/EnvironmentManager.kt +++ b/src/nl/hannahsten/texifyidea/lang/alias/EnvironmentManager.kt @@ -1,7 +1,13 @@ package nl.hannahsten.texifyidea.lang.alias +import arrow.core.nonEmptyListOf +import nl.hannahsten.texifyidea.lang.DefaultEnvironment +import nl.hannahsten.texifyidea.lang.LabelingEnvironmentInformation +import nl.hannahsten.texifyidea.lang.commands.LatexNewDefinitionCommand import nl.hannahsten.texifyidea.psi.LatexCommands import nl.hannahsten.texifyidea.util.containsAny +import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic +import nl.hannahsten.texifyidea.util.magic.cmd import nl.hannahsten.texifyidea.util.parser.requiredParameter /** @@ -9,15 +15,35 @@ import nl.hannahsten.texifyidea.util.parser.requiredParameter */ object EnvironmentManager : AliasManager() { + /** + * Maintain information about label parameter locations of environments for which that is applicable. + * Maps environment name to parameter index of the \begin command, starting from 0 but including the first parameter which is the environment name + */ + val labelAliasesInfo = mutableMapOf() + override fun findAllAliases(aliasSet: Set, indexedDefinitions: Collection) { val firstAlias = aliasSet.first() // Assume the environment that is defined is the first parameter, and that the first part of the definition is in the second // e.g. \newenvironment{mytabl}{\begin{tabular}{cc}}{\end{tabular}} - indexedDefinitions.filter { definition -> + val definitions = indexedDefinitions.filter { definition -> definition.requiredParameter(1)?.containsAny(aliasSet.map { "\\begin{$it}" }.toSet()) == true + // This command always defines an alias for the listings environment + || (definition.name == LatexNewDefinitionCommand.LSTNEWENVIRONMENT.cmd && aliasSet.contains(DefaultEnvironment.LISTINGS.environmentName)) } + definitions .mapNotNull { it.requiredParameter(0) } .forEach { registerAlias(firstAlias, it) } + + // Update label parameter position information + if (aliasSet.intersect(EnvironmentMagic.labelAsParameter).isNotEmpty()) { + definitions.forEach { + val definedEnvironment = it.requiredParameter(0) ?: return@forEach + // The label may be in an optional parameter of an environment, but it may also be in other places like a \lstset, so for now we do a text-based search + val text = it.requiredParameter(1) ?: return@forEach + val index = "label\\s*=\\s*\\{?\\s*#(\\d)".toRegex().find(text)?.groupValues?.getOrNull(1)?.toInt() ?: return@forEach + labelAliasesInfo[definedEnvironment] = LabelingEnvironmentInformation(nonEmptyListOf(index)) + } + } } } \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/lang/commands/LatexNewDefinitionCommand.kt b/src/nl/hannahsten/texifyidea/lang/commands/LatexNewDefinitionCommand.kt index 55009a442..73c5c745e 100644 --- a/src/nl/hannahsten/texifyidea/lang/commands/LatexNewDefinitionCommand.kt +++ b/src/nl/hannahsten/texifyidea/lang/commands/LatexNewDefinitionCommand.kt @@ -1,6 +1,7 @@ package nl.hannahsten.texifyidea.lang.commands import nl.hannahsten.texifyidea.lang.LatexPackage +import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.LISTINGS import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.TCOLORBOX import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.XARGS @@ -38,6 +39,7 @@ enum class LatexNewDefinitionCommand( DECLAREROBUSTCOMMANDX("DeclareRobustCommandx", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Argument.Type.TEXT), dependency = XARGS), NEWENVIRONMENTX("newenvironmentx", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "begdef".asRequired(Argument.Type.TEXT), "enddef".asRequired(Argument.Type.TEXT), dependency = XARGS), RENEWENVIRONMENTX("renewenvironmentx", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "begdef".asRequired(Argument.Type.TEXT), "enddef".asRequired(Argument.Type.TEXT), dependency = XARGS), + LSTNEWENVIRONMENT("lstnewenvironment", "name".asRequired(), "number".asOptional(), "default arg".asOptional(), "starting code".asRequired(), "ending code".asRequired(), dependency = LISTINGS), ; override val identifier: String diff --git a/src/nl/hannahsten/texifyidea/psi/LatexEnvironmentUtil.kt b/src/nl/hannahsten/texifyidea/psi/LatexEnvironmentUtil.kt index 3ca79ec66..276234f80 100644 --- a/src/nl/hannahsten/texifyidea/psi/LatexEnvironmentUtil.kt +++ b/src/nl/hannahsten/texifyidea/psi/LatexEnvironmentUtil.kt @@ -9,12 +9,10 @@ import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic import nl.hannahsten.texifyidea.util.parser.getOptionalParameterMapFromParameters import nl.hannahsten.texifyidea.util.parser.toStringMap -/* -* LatexEnvironment -*/ /** * Find the label of the environment. The method finds labels inside the environment content as well as labels * specified via an optional parameter + * Similar to LabelExtraction#extractLabelElement, but we cannot use the index here * * @return the label name if any, null otherwise */ diff --git a/src/nl/hannahsten/texifyidea/reference/LatexLabelReference.kt b/src/nl/hannahsten/texifyidea/reference/LatexLabelReference.kt index 7b92e0acb..c79606df0 100644 --- a/src/nl/hannahsten/texifyidea/reference/LatexLabelReference.kt +++ b/src/nl/hannahsten/texifyidea/reference/LatexLabelReference.kt @@ -66,8 +66,8 @@ class LatexLabelReference(element: LatexCommands, range: TextRange?) : PsiRefere val allCommands = file.commandsInFileSet() return file.findLatexLabelingElementsInFileSet() .toSet() - .mapNotNull { labelingCommand: PsiElement -> - val extractedLabel = labelingCommand.extractLabelName(referencingFileSetCommands = allCommands) + .mapNotNull { labelingElement: PsiElement -> + val extractedLabel = labelingElement.extractLabelName(referencingFileSetCommands = allCommands) if (extractedLabel.isBlank()) return@mapNotNull null LookupElementBuilder @@ -75,11 +75,11 @@ class LatexLabelReference(element: LatexCommands, range: TextRange?) : PsiRefere .bold() .withInsertHandler(LatexReferenceInsertHandler()) .withTypeText( - labelingCommand.containingFile.name + ":" + + labelingElement.containingFile.name + ":" + ( 1 + StringUtil.offsetToLineNumber( - labelingCommand.containingFile.text, - labelingCommand.textOffset + labelingElement.containingFile.text, + labelingElement.textOffset ) ), true diff --git a/src/nl/hannahsten/texifyidea/util/labels/LabelExtraction.kt b/src/nl/hannahsten/texifyidea/util/labels/LabelExtraction.kt index 6fa734a9d..3ad2d006b 100644 --- a/src/nl/hannahsten/texifyidea/util/labels/LabelExtraction.kt +++ b/src/nl/hannahsten/texifyidea/util/labels/LabelExtraction.kt @@ -3,6 +3,7 @@ package nl.hannahsten.texifyidea.util.labels import com.intellij.psi.PsiElement import com.jetbrains.rd.util.first import nl.hannahsten.texifyidea.lang.alias.CommandManager +import nl.hannahsten.texifyidea.lang.alias.EnvironmentManager import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand import nl.hannahsten.texifyidea.psi.* import nl.hannahsten.texifyidea.util.magic.CommandMagic @@ -14,6 +15,7 @@ import nl.hannahsten.texifyidea.util.parser.toStringMap /** * Extracts the label element (so the element that should be resolved to) from the PsiElement given that the PsiElement represents a label. + * Also see LatexEnvironmentUtil#getLabel() */ fun PsiElement.extractLabelElement(): PsiElement? { fun getLabelParameterText(command: LatexCommandWithParams): LatexParameterText { @@ -45,7 +47,14 @@ fun PsiElement.extractLabelElement(): PsiElement? { getLabelParameterText(beginCommand) } else { - null + // Check for user defined environments + val labelPositions = EnvironmentManager.labelAliasesInfo.getOrDefault(getEnvironmentName(), null) + if (labelPositions != null) { + this.beginCommand.parameterList.getOrNull(labelPositions.positions.first())?.firstChildOfType(LatexParameterText::class) + } + else { + null + } } } else -> null @@ -86,7 +95,14 @@ fun PsiElement.extractLabelName(referencingFileSetCommands: Collection this.getLabel() ?: "" + is LatexEnvironment -> { + this.getLabel() + // Check if it is a user defined alias of a labeled environment + ?: EnvironmentManager.labelAliasesInfo.getOrDefault(getEnvironmentName(), null)?.let { + this.beginCommand.parameterList.getOrNull(it.positions.first())?.firstChildOfType(LatexParameterText::class)?.text + } + ?: "" + } else -> text } } \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/util/labels/Labels.kt b/src/nl/hannahsten/texifyidea/util/labels/Labels.kt index a80998d7a..66cbb6ef2 100644 --- a/src/nl/hannahsten/texifyidea/util/labels/Labels.kt +++ b/src/nl/hannahsten/texifyidea/util/labels/Labels.kt @@ -3,14 +3,18 @@ package nl.hannahsten.texifyidea.util.labels import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import nl.hannahsten.texifyidea.index.LatexEnvironmentsIndex import nl.hannahsten.texifyidea.index.LatexParameterLabeledCommandsIndex import nl.hannahsten.texifyidea.index.LatexParameterLabeledEnvironmentsIndex +import nl.hannahsten.texifyidea.lang.alias.EnvironmentManager import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand import nl.hannahsten.texifyidea.psi.LatexCommands +import nl.hannahsten.texifyidea.psi.getEnvironmentName import nl.hannahsten.texifyidea.reference.InputFileReference import nl.hannahsten.texifyidea.util.files.commandsInFile import nl.hannahsten.texifyidea.util.files.commandsInFileSet import nl.hannahsten.texifyidea.util.files.psiFile +import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic /** * Finds all the defined labels in the fileset of the file. @@ -46,9 +50,19 @@ fun PsiFile.findLatexLabelingElementsInFile(): Sequence = sequenceOf fun PsiFile.findLatexLabelingElementsInFileSet(): Sequence = sequenceOf( findLabelingCommandsInFileSet(), LatexParameterLabeledEnvironmentsIndex.Util.getItemsInFileSet(this).asSequence(), - LatexParameterLabeledCommandsIndex.Util.getItemsInFileSet(this).asSequence() + LatexParameterLabeledCommandsIndex.Util.getItemsInFileSet(this).asSequence(), + findLabeledEnvironments(this), ).flatten() +/** + * All environments with labels, including user defined + */ +fun findLabeledEnvironments(file: PsiFile): Sequence { + EnvironmentManager.updateAliases(EnvironmentMagic.labelAsParameter, file.project) + val allEnvironments = EnvironmentManager.getAliases(EnvironmentMagic.labelAsParameter.first()) + return LatexEnvironmentsIndex.Util.getItemsInFileSet(file).filter { it.getEnvironmentName() in allEnvironments }.asSequence() +} + /** * Make a sequence of all commands in the file set that specify a label. This does not include commands which define a label via an * optional parameter. diff --git a/src/nl/hannahsten/texifyidea/util/magic/CommandMagic.kt b/src/nl/hannahsten/texifyidea/util/magic/CommandMagic.kt index 948615a13..3adf327dd 100644 --- a/src/nl/hannahsten/texifyidea/util/magic/CommandMagic.kt +++ b/src/nl/hannahsten/texifyidea/util/magic/CommandMagic.kt @@ -326,6 +326,7 @@ object CommandMagic { NEWTCOLORBOX_, PROVIDETCOLORBOX, NEWENVIRONMENTX, + LSTNEWENVIRONMENT, ).map { it.cmd } /** diff --git a/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexUnresolvedReferenceInspectionTest.kt b/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexUnresolvedReferenceInspectionTest.kt index 757fcc659..368098172 100644 --- a/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexUnresolvedReferenceInspectionTest.kt +++ b/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexUnresolvedReferenceInspectionTest.kt @@ -5,6 +5,7 @@ import io.mockk.mockkStatic import nl.hannahsten.texifyidea.file.LatexFileType import nl.hannahsten.texifyidea.inspections.TexifyInspectionTestBase import nl.hannahsten.texifyidea.lang.alias.CommandManager +import nl.hannahsten.texifyidea.lang.alias.EnvironmentManager import nl.hannahsten.texifyidea.util.runCommandWithExitCode import org.junit.Test @@ -100,6 +101,35 @@ class LatexUnresolvedReferenceInspectionTest : TexifyInspectionTestBase(LatexUnr myFixture.checkHighlighting() } + fun testFigureReferencedCustomListingsEnvironment() { + myFixture.configureByText( + LatexFileType, + """ + \lstnewenvironment{java}[2][]{ + \lstset{ + captionpos=b, + language=Java, + % other style attributes + caption={#1}, + label={#2}, + } + }{} + + \begin{java}[Test]{lst:test} + class Main { + public static void main(String[] args) { + return "HelloWorld"; + } + } + \end{java} + + \ref{lst:test} + """.trimIndent() + ) + EnvironmentManager.updateAliases(setOf("lstlisting"), project) + myFixture.checkHighlighting() + } + fun testComma() { myFixture.configureByText(LatexFileType, """\input{name,with,.tex}""") myFixture.checkHighlighting()