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

Support label references to user defined listings environment #3825

Merged
merged 4 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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 @@
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 @@

/**
* 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 @@
getLabelParameterText(beginCommand)
}
else {
null
// Check for user defined environments
val labelPositions = EnvironmentManager.labelAliasesInfo.get(getEnvironmentName())

Check notice on line 51 in src/nl/hannahsten/texifyidea/util/labels/LabelExtraction.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Explicit 'get' or 'set' call

Should be replaced with indexing
Fixed Show fixed Hide fixed
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 @@
}
}

is LatexEnvironment -> this.getLabel() ?: ""
is LatexEnvironment -> {
this.getLabel()
// Check if it is a user defined alias of a labeled environment
?: EnvironmentManager.labelAliasesInfo.get(getEnvironmentName())?.let {

Check notice on line 101 in src/nl/hannahsten/texifyidea/util/labels/LabelExtraction.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Explicit 'get' or 'set' call

Should be replaced with indexing
Fixed Show fixed Hide fixed
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
Loading