diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index dd4125b2b..44da70467 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -500,6 +500,9 @@ + diff --git a/resources/inspectionDescriptions/LatexIncorrectSectionNesting.html b/resources/inspectionDescriptions/LatexIncorrectSectionNesting.html new file mode 100644 index 000000000..50f270ce3 --- /dev/null +++ b/resources/inspectionDescriptions/LatexIncorrectSectionNesting.html @@ -0,0 +1,5 @@ + + + It is encouraged to use subsection only within section, subparagraph in paragraph and so on. + + diff --git a/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt new file mode 100644 index 000000000..b4192b377 --- /dev/null +++ b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt @@ -0,0 +1,90 @@ +package nl.hannahsten.texifyidea.inspections.latex + +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.LatexCommandsIndex +import nl.hannahsten.texifyidea.insight.InsightGroup +import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase +import nl.hannahsten.texifyidea.psi.LatexCommands +import nl.hannahsten.texifyidea.util.files.document +import nl.hannahsten.texifyidea.util.files.openedEditor +import nl.hannahsten.texifyidea.util.lineIndentation +import nl.hannahsten.texifyidea.util.replaceString + +/** + * @author Johannes Berger + */ +open class LatexIncorrectSectionNestingInspection : TexifyInspectionBase() { + + override val inspectionGroup = InsightGroup.LATEX + + override val inspectionId = "IncorrectSectionNesting" + + override fun getDisplayName() = "Incorrect nesting" + + override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List { + + return LatexCommandsIndex.getCommandsByNames(file, *sectioningCommands()) + .sortedBy { it.textOffset } + .zipWithNext() + .filter { (first, second) -> first.commandName() in commandToForbiddenPredecessors[second.commandName()] ?: error("Unexpected command") } + .map { manager.createProblemDescriptor(it.second, + "Incorrect nesting", + arrayOf(InsertParentCommandFix(), ChangeToParentCommandFix()), + ProblemHighlightType.WEAK_WARNING, + isOntheFly, + false) + } + } + + private fun sectioningCommands() = commandToForbiddenPredecessors.keys.toTypedArray() + + private fun LatexCommands.commandName(): String = this.commandToken.text + + private class InsertParentCommandFix : LocalQuickFix { + + override fun getFamilyName() = "Insert missing parent command" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val command = descriptor.psiElement as LatexCommands + val document = command.containingFile.document() ?: return + val offset = command.textOffset + val lineNumber = document.getLineNumber(offset) + val newParentCommand = command.commandToken.text.replaceFirst("sub", "") + val replacement = "$newParentCommand{}\n${document.lineIndentation(lineNumber)}" + val caret = command.containingFile.openedEditor()?.caretModel + document.insertString(offset, replacement) + caret?.moveToOffset(offset + newParentCommand.length + 1) + } + } + + private class ChangeToParentCommandFix : LocalQuickFix { + + override fun getFamilyName() = "Change to parent command" + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val command = descriptor.psiElement as LatexCommands + val document = command.containingFile.document() ?: return + val range = command.commandToken.textRange + val newParentCommand = command.commandToken.text.replaceFirst("sub", "") + document.replaceString(range, newParentCommand) + } + } + + companion object { + + val commandToForbiddenPredecessors = mapOf( + """\part""" to emptyList(), + """\chapter""" to emptyList(), + """\section""" to emptyList(), + """\subsection""" to listOf("""\part""", """\chapter"""), + """\subsubsection""" to listOf("""\part""", """\chapter""", """\section"""), + """\paragraph""" to emptyList(), + """\subparagraph""" to listOf("""\part""", """\chapter""", """\section""", """\subsection""", """\subsubsection""") + ) + } +} diff --git a/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt b/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt new file mode 100644 index 000000000..c01bb978b --- /dev/null +++ b/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt @@ -0,0 +1,130 @@ +package nl.hannahsten.texifyidea.inspections.latex + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import nl.hannahsten.texifyidea.file.LatexFileType +import nl.hannahsten.texifyidea.testutils.writeCommand +import org.junit.Test + +internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase() { + + override fun setUp() { + super.setUp() + myFixture.enableInspections(LatexIncorrectSectionNestingInspection()) + } + + @Test + fun `test document missing subsection warning`() { + testInsertMissingParentCommandQuickFix(""" + \begin{document} + \section{} + \subsubsection{} + \end{document} + """.trimIndent(), """ + \begin{document} + \section{} + \subsection{} + \subsubsection{} + \end{document} + """.trimIndent()) + } + + @Test + fun `test subsection after chapter warning`() { + testInsertMissingParentCommandQuickFix(""" + \begin{document} + \chapter{} + \subsection{} + \end{document} + """.trimIndent(), """ + \begin{document} + \chapter{} + \section{} + \subsection{} + \end{document} + """.trimIndent()) + } + + @Test + fun `test change subsubsection to subsection quick fix`() { + testChangeToParentCommandQuickFix(""" + \begin{document} + \section{} + \subsubsection{} + \end{document} + """.trimIndent(), """ + \begin{document} + \section{} + \subsection{} + \end{document} + """.trimIndent()) + } + + @Test + fun `test subparagraph after section warning`() { + testInsertMissingParentCommandQuickFix(""" + \begin{document} + \section{} + \subparagraph{} + \end{document} + """.trimIndent(), """ + \begin{document} + \section{} + \paragraph{} + \subparagraph{} + \end{document} + """.trimIndent()) + } + + @Test + fun `test missing parent command warning`() { + myFixture.configureByText(LatexFileType, """ + \begin{document} + \section{} + \subsubsection{} + \end{document} + """.trimIndent()) + myFixture.checkHighlighting(false, false, true, false) + } + + @Test + fun `test no warning on correct nesting`() { + myFixture.configureByText(LatexFileType, """ + \begin{document} + \part{} + \part{} + \chapter{} + \section{} + \section{} + \subsection{} + \subsubsection{} + \subsubsection{} + \section{} + \paragraph{} + \paragraph{} + \subparagraph{} + \subparagraph{} + \end{document} + """.trimIndent()) + myFixture.checkHighlighting(false, false, true, false) + } + + private fun testInsertMissingParentCommandQuickFix(before: String, after: String) { + myFixture.configureByText(LatexFileType, before) + val quickFixes = myFixture.getAllQuickFixes() + writeCommand(myFixture.project) { + quickFixes.first { it.familyName == "Insert missing parent command" }.invoke(myFixture.project, myFixture.editor, myFixture.file) + } + + myFixture.checkResult(after) + } + + private fun testChangeToParentCommandQuickFix(before: String, after: String) { + myFixture.configureByText(LatexFileType, before) + val quickFixes = myFixture.getAllQuickFixes() + writeCommand(myFixture.project) { + quickFixes.first { it.familyName == "Change to parent command" }.invoke(myFixture.project, myFixture.editor, myFixture.file) + } + + myFixture.checkResult(after) + } +} \ No newline at end of file