Skip to content

Commit

Permalink
Merge pull request #3825 from Hannah-Sten/custom-env-labels
Browse files Browse the repository at this point in the history
Support label references to user defined listings environment
  • Loading branch information
PHPirates authored Dec 28, 2024
2 parents b410288 + 1b269be commit c748e1f
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Writerside/topics/Code-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Web links in `\url` and `\href` commands are clickable using <shortcut>Ctrl + Cl
By pressing <shortcut>Ctrl + B</shortcut> 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 <shortcut>Ctrl + Alt + &lt;-</shortcut>

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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Int>,
/** Default label prefix, for example in \newcommand{\mylabel}[1]{\label{sec:#1}} it would be sec: */
var prefix: String = ""
)
28 changes: 27 additions & 1 deletion src/nl/hannahsten/texifyidea/lang/alias/EnvironmentManager.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
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

/**
* Similar to the [CommandManager], this manages aliases of environments.
*/
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<String, LabelingEnvironmentInformation>()

override fun findAllAliases(aliasSet: Set<String>, indexedDefinitions: Collection<LatexCommands>) {
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))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions src/nl/hannahsten/texifyidea/psi/LatexEnvironmentUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
10 changes: 5 additions & 5 deletions src/nl/hannahsten/texifyidea/reference/LatexLabelReference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,20 @@ 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
.create(extractedLabel)
.bold()
.withInsertHandler(LatexReferenceInsertHandler())
.withTypeText(
labelingCommand.containingFile.name + ":" +
labelingElement.containingFile.name + ":" +
(
1 + StringUtil.offsetToLineNumber(
labelingCommand.containingFile.text,
labelingCommand.textOffset
labelingElement.containingFile.text,
labelingElement.textOffset
)
),
true
Expand Down
20 changes: 18 additions & 2 deletions src/nl/hannahsten/texifyidea/util/labels/LabelExtraction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -86,7 +95,14 @@ fun PsiElement.extractLabelName(referencingFileSetCommands: Collection<LatexComm
}
}

is LatexEnvironment -> 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
}
}
16 changes: 15 additions & 1 deletion src/nl/hannahsten/texifyidea/util/labels/Labels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -46,9 +50,19 @@ fun PsiFile.findLatexLabelingElementsInFile(): Sequence<PsiElement> = sequenceOf
fun PsiFile.findLatexLabelingElementsInFileSet(): Sequence<PsiElement> = 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<PsiElement> {
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.
Expand Down
1 change: 1 addition & 0 deletions src/nl/hannahsten/texifyidea/util/magic/CommandMagic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ object CommandMagic {
NEWTCOLORBOX_,
PROVIDETCOLORBOX,
NEWENVIRONMENTX,
LSTNEWENVIRONMENT,
).map { it.cmd }

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit c748e1f

Please sign in to comment.