diff --git a/resources/META-INF/extensions/inspections/latex/probablebugs/probablebugs.xml b/resources/META-INF/extensions/inspections/latex/probablebugs/probablebugs.xml index 3456839f5..e7e978535 100644 --- a/resources/META-INF/extensions/inspections/latex/probablebugs/probablebugs.xml +++ b/resources/META-INF/extensions/inspections/latex/probablebugs/probablebugs.xml @@ -24,6 +24,10 @@ groupPath="LaTeX" groupName="Probable bugs" displayName="Unresolved references" enabledByDefault="true" level="WARNING" /> + + +Reports usages of ~ and \\ in a Section-like command and suggests to add an optional argument to the +command. +

+ This inspection is based on the following paragraph from The LaTeX Companion (2nd edition, page 23): +

+ If you try to advise TeX on how to split the heading over a few lines using the '~' symbol so the '\\' command, then + side effects may result when formatting the table of contents or generating the running head. + In this case the simplest solution is to repeat the heading text without the specific markup in the optional + parameter of the sectioning command. + + + \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexSuspiciousSectionFormattingInspection.kt b/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexSuspiciousSectionFormattingInspection.kt new file mode 100644 index 000000000..5719a44c4 --- /dev/null +++ b/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexSuspiciousSectionFormattingInspection.kt @@ -0,0 +1,80 @@ +package nl.hannahsten.texifyidea.inspections.latex.probablebugs + +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.openapi.util.TextRange +import com.intellij.psi.PsiFile +import com.intellij.webSymbols.references.WebSymbolReferenceProvider.Companion.startOffsetIn +import nl.hannahsten.texifyidea.inspections.InsightGroup +import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase +import nl.hannahsten.texifyidea.psi.LatexCommands +import nl.hannahsten.texifyidea.psi.LatexPsiHelper +import nl.hannahsten.texifyidea.psi.LatexRequiredParam +import nl.hannahsten.texifyidea.util.containsAny +import nl.hannahsten.texifyidea.util.files.commandsInFile +import nl.hannahsten.texifyidea.util.firstChildOfType +import nl.hannahsten.texifyidea.util.magic.CommandMagic +import nl.hannahsten.texifyidea.util.requiredParameter + +open class LatexSuspiciousSectionFormattingInspection : TexifyInspectionBase() { + + override val inspectionGroup = InsightGroup.LATEX + + override fun getDisplayName() = "Suspicious formatting in the required argument of a sectioning command" + + override val inspectionId = "SuspiciousSectionFormatting" + + override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List { + return file.commandsInFile() + .asSequence() + .filter { it.name in CommandMagic.sectionMarkers } + .filter { it.optionalParameterMap.isEmpty() } + .filter { it.requiredParameter(0)?.containsAny(formatting) == true } + .map { psiElement -> + val requiredParam = psiElement.firstChildOfType(LatexRequiredParam::class) + // Plus 1 for the opening brace. + val startOffset = requiredParam?.startOffsetIn(psiElement)?.plus(1) ?: 0 + // Minus 2 for the braces surrounding the parameter. + val endOffset = requiredParam?.textLength?.minus(2)?.plus(startOffset) ?: psiElement.textLength + manager.createProblemDescriptor( + psiElement, + TextRange(startOffset, endOffset), + "Suspicious formatting in ${psiElement.name}", + ProblemHighlightType.WARNING, + isOntheFly, + AddOptionalArgumentQuickFix() + ) + } + .toList() + } + + class AddOptionalArgumentQuickFix : LocalQuickFix { + + override fun getFamilyName(): String { + return "Fix formatting in table of contents and running head" + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val command = descriptor.psiElement as LatexCommands + val requiredParamText = command.requiredParameter(0) + val optionalParamText = requiredParamText?.replace(Regex(formatting.joinToString("", prefix = "[", postfix = "]")), " ") + ?: return + val optionalArgument = LatexPsiHelper(project).createOptionalParameter(optionalParamText) + + command.addAfter(optionalArgument, command.commandToken) + // Create a new command and completely replace the old command so all the psi methods will recompute instead + // of using old values from their cache. + val newCommand = LatexPsiHelper(project).createFromText(command.text).firstChildOfType(LatexCommands::class) + ?: return + command.replace(newCommand) + } + } + + companion object { + + val formatting = setOf("~", "\\\\") + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/psi/LatexPsiHelper.kt b/src/nl/hannahsten/texifyidea/psi/LatexPsiHelper.kt index 66c5a71ef..5180bcf0e 100644 --- a/src/nl/hannahsten/texifyidea/psi/LatexPsiHelper.kt +++ b/src/nl/hannahsten/texifyidea/psi/LatexPsiHelper.kt @@ -84,6 +84,11 @@ class LatexPsiHelper(private val project: Project) { return createFromText(commandText).firstChildOfType(LatexRequiredParam::class)!! } + fun createOptionalParameter(content: String): LatexOptionalParam { + val commandText = "\\section[$content]{$content}" + return createFromText(commandText).firstChildOfType(LatexOptionalParam::class)!! + } + /** * Returns the LatexOptionalParam node that is supposed to contain the label key for the command. * If no such node exists yet, a new one is created at the correct position. diff --git a/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexSuspiciousSectionFormattingInspectionTest.kt b/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexSuspiciousSectionFormattingInspectionTest.kt new file mode 100644 index 000000000..e5fc4c733 --- /dev/null +++ b/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/LatexSuspiciousSectionFormattingInspectionTest.kt @@ -0,0 +1,73 @@ +package nl.hannahsten.texifyidea.inspections.latex.probablebugs + +import nl.hannahsten.texifyidea.file.LatexFileType +import nl.hannahsten.texifyidea.inspections.TexifyInspectionTestBase + +internal class LatexSuspiciousSectionFormattingInspectionTest : TexifyInspectionTestBase(LatexSuspiciousSectionFormattingInspection()) { + + fun `test ~ warning`() { + myFixture.configureByText( + LatexFileType, + "\\section{You should not use~in the title of a section}" + ) + myFixture.checkHighlighting(true, false, true, false) + } + + fun `test ~ warning for short section`() { + myFixture.configureByText( + LatexFileType, + "\\section{a~b}" + ) + myFixture.checkHighlighting(true, false, true, false) + } + + fun `test backslash warning`() { + myFixture.configureByText( + LatexFileType, + "\\section{You should not use\\\\in the title of a section}" + ) + myFixture.checkHighlighting(true, false, true, false) + } + + fun `test multiple warnings in one section`() { + myFixture.configureByText( + LatexFileType, + "\\section{You should not use~in the title~of a section}" + ) + myFixture.checkHighlighting(true, false, true, false) + } + + fun `test no warning for ~ when optional argument is present`() { + myFixture.configureByText( + LatexFileType, + "\\section[Table of contents long title]{Title with explicit~formatting}" + ) + myFixture.checkHighlighting(true, false, true, false) + } + + fun `test no warning for backslash when optional argument is present`() { + myFixture.configureByText( + LatexFileType, + "\\section[Table of contents long title]{Title with explicit \\\\ formatting}" + ) + myFixture.checkHighlighting(true, false, true, false) + } + + fun `test simple quickfix for ~`() { + myFixture.configureByText( + LatexFileType, + "\\section{You should not use~in the title}" + ) + testQuickFix("\\section{You should not use~in the title}", "\\section[You should not use in the title]{You should not use~in the title}") + myFixture.checkHighlighting(true, false, true, false) + } + + fun `test simple quickfix for backslash`() { + myFixture.configureByText( + LatexFileType, + "\\section{You should not use~in the title}" + ) + testQuickFix("\\section{You should not use \\\\ in the title}", "\\section[You should not use in the title]{You should not use \\\\ in the title}") + myFixture.checkHighlighting(true, false, true, false) + } +}