Skip to content

Commit

Permalink
Merge pull request #3806 from Hannah-Sten/glossary-usages
Browse files Browse the repository at this point in the history
Add inspection to warn about a missing reference for a glossary occurrence
  • Loading branch information
PHPirates authored Dec 14, 2024
2 parents 5f0c25c + 14ed3b6 commit 2f81fdb
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 1 deletion.
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
* Add inspection to warn about a missing reference for a glossary occurrence
* Do not fold sections in a command definition
* Include optional parameters in spellcheck, if it contains text

Expand Down
8 changes: 7 additions & 1 deletion Writerside/topics/Typesetting-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,10 @@ For example, instead of `(\frac 1 2)` write `\left(\frac 1 2\right)`.

## Citations must be placed before interpunction

Use `Sentence~\cite{knuth1990}.` and not `Sentence.~\cite{knuth1990}`
Use `Sentence~\cite{knuth1990}.` and not `Sentence.~\cite{knuth1990}`

## Missing glossary reference

When using a glossary, it is good practice to reference every glossary entry with a \gls-like command.
This makes sure that the list of pages with occurrences in the glossary is complete.
For examples on how to use a glossary, see [External tools](External-tools.md#glossary-examples).
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,9 @@
groupPath="LaTeX" groupName="Typesetting issues" displayName="Incorrectly typeset quotation marks"
enabledByDefault="true"
level="WARNING" />
<localInspection language="Latex" implementationClass="nl.hannahsten.texifyidea.inspections.latex.typesetting.LatexMissingGlossaryReferenceInspection"
groupPath="LaTeX" groupName="Code maturity" displayName="Missing glossary reference"
enabledByDefault="true"
level="WARNING" />
</extensions>
</idea-plugin>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<body>
Reports occurrences of glossary entries that are not marked with a \gls command.
<!-- tooltip end -->
<p>
When using a glossary, it is good practice to reference every glossary entry with a \gls-like command.
This makes sure that the list of pages with occurrences in the glossary is complete.
</p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package nl.hannahsten.texifyidea.inspections.latex.typesetting

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexGlossaryEntryIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.commands.LatexGlossariesCommand
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.toTextRange

/**
* Glossary entries should be referenced for all occurrences.
*/
class LatexMissingGlossaryReferenceInspection : TexifyInspectionBase() {
override val inspectionGroup = InsightGroup.LATEX
override val inspectionId = "MissingGlossaryReference"
override fun getDisplayName() = "Missing glossary reference"

override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
val descriptors = mutableListOf<ProblemDescriptor>()
val names = LatexGlossaryEntryIndex.Util.getItemsInFileSet(file).mapNotNull { LatexGlossariesCommand.extractGlossaryName(it) }
// Unfortunately the lowest level we have is a block of text, so we have to do a text-based search
file.childrenOfType<LatexNormalText>().forEach { textElement ->
val text = textElement.text
names.forEach { name ->
val correctOccurrences = "\\\\gls[^{]+\\{($name)}".toRegex().findAll(text).mapNotNull { it.groups.firstOrNull()?.range }
val allOccurrences = name.toRegex().findAll(text).map { it.range }
allOccurrences.filter { !correctOccurrences.contains(it) }.forEach { range ->
descriptors.add(
manager.createProblemDescriptor(
textElement,
range.toTextRange(),
"Missing glossary reference",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
isOntheFly,
AddGlsFix(),
)
)
}
}
}
return descriptors
}

private class AddGlsFix : LocalQuickFix {
override fun getFamilyName() = "Add \\gls command"

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val range = descriptor.textRangeInElement
val newText = descriptor.psiElement.text.replaceRange(range.endOffset, range.endOffset, "}")
.replaceRange(range.startOffset, range.startOffset, "\\gls{")

val newElement = LatexPsiHelper(project).createFromText(newText).firstChild
descriptor.psiElement.replace(newElement)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import nl.hannahsten.texifyidea.util.parser.requiredParameters

enum class LatexGlossariesCommand(
Expand Down Expand Up @@ -80,5 +82,21 @@ enum class LatexGlossariesCommand(
if (!CommandMagic.glossaryEntry.contains(command.name)) return null
return command.requiredParameters()[0].firstChildOfType(LatexParameterText::class)
}

/**
* Find the name, which is the text that will appear in the document, from the given glossary entry definition.
*/
fun extractGlossaryName(command: LatexCommands): String? {
if (setOf(NEWGLOSSARYENTRY, LONGNEWGLOSSARYENTRY).map { it.cmd }.contains(command.name)) {
val keyValueList = command.requiredParameter(1) ?: return null
return "name=\\{([^}]+)}".toRegex().find(keyValueList)?.groupValues?.get(1)
}
else if (setOf(NEWACRONYM, NEWABBREVIATION).map { it.cmd }.contains(command.name)) {
return command.requiredParameter(1)
}
else {
return null
}
}
}
}
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 @@ -193,6 +193,7 @@ object CommandMagic {

/**
* All commands that define a glossary entry of the glossaries package (e.g. \newacronym).
* When adding a command, define how to get the glossary name in [nl.hannahsten.texifyidea.lang.commands.LatexGlossariesCommand.extractGlossaryName].
*/
val glossaryEntry =
hashSetOf(NEWGLOSSARYENTRY, LONGNEWGLOSSARYENTRY, NEWACRONYM, NEWABBREVIATION).map { it.cmd }.toSet()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nl.hannahsten.texifyidea.inspections.latex.typesetting

import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.inspections.TexifyInspectionTestBase

class LatexMissingGlossaryReferenceInspectionTest : TexifyInspectionTestBase(LatexMissingGlossaryReferenceInspection()) {

fun testMissingReference() {
myFixture.configureByText(LatexFileType, """\newglossaryentry{sample}{name={sample},description={an example}} \gls{sample} \Glslink{sample} <warning descr="Missing glossary reference">sample</warning>""")
myFixture.checkHighlighting()
}

fun testAddGls() {
testQuickFix(
"""
\newglossaryentry{sample}{name={sample},description={an example}} \gls{sample} sample text
""".trimIndent(),
"""
\newglossaryentry{sample}{name={sample},description={an example}} \gls{sample} \gls{sample} text
""".trimIndent()
)
}
}

0 comments on commit 2f81fdb

Please sign in to comment.