From c837f0b028a3c56f12944d6ca493d17e1a5eeba2 Mon Sep 17 00:00:00 2001 From: johannesberger Date: Sun, 21 Jun 2020 01:19:43 +0200 Subject: [PATCH 1/5] Fixes 1490: Inspection notices misplaced section and paragraph commands and offers quickfix --- resources/META-INF/plugin.xml | 3 + .../LatexIncorrectSectionNesting.html | 5 + .../LatexIncorrectSectionNestingInspection.kt | 80 ++++++++++++ ...exIncorrectSectionNestingInspectionTest.kt | 114 ++++++++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 resources/inspectionDescriptions/LatexIncorrectSectionNesting.html create mode 100644 src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt create mode 100644 test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 9089f459e..eb2ef4730 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -564,6 +564,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..9223ab831 --- /dev/null +++ b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt @@ -0,0 +1,80 @@ +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.Magic +import nl.hannahsten.texifyidea.util.files.document +import nl.hannahsten.texifyidea.util.files.openedEditor +import nl.hannahsten.texifyidea.util.lineIndentation + +/** + * @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 { + val descriptors = descriptorList() + + val sectionCommands = LatexCommandsIndex.getCommandsByNames(file, "\\section", "\\subsection", "\\subsubsection", "\\paragraph", "\\subparagraph").sortedBy { it.textOffset } + sectionCommands.forEachIndexed { index, command -> + if (startsWithSubCommand(command, index) + || subsubsectionAftersection(command, sectionCommands, index) + || subParagraphWithoutParagraph(command, sectionCommands, index)) { + descriptors.add(manager.createProblemDescriptor(command, "Incorrect nesting", InsertParentCommandFix(), ProblemHighlightType.WARNING, isOntheFly)) + } + } + return descriptors + } + + private fun startsWithSubCommand(command: LatexCommands, index: Int): Boolean { + val level = command.level() + return ((level == 2 || level == 3 || level == 5) && index == 0) + } + + private fun subsubsectionAftersection(command: LatexCommands, sectionCommands: List, index: Int) = + command.level() == 3 && sectionCommands[index - 1].level() == 1 + + private fun subParagraphWithoutParagraph(command: LatexCommands, sectionCommands: List, index: Int) = + (command.level() == 5 && sectionCommands[index - 1].level() < 4) + + private fun LatexCommands.level(): Int { + return Magic.Command.labeledLevels + .filterKeys { it.command == this.commandToken.text.removePrefix("\\") } + .map { it.value } + .firstOrNull() ?: error("Unexpected command") + } + + 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) + + } + } + +} + 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..314db38b2 --- /dev/null +++ b/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt @@ -0,0 +1,114 @@ +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`() { + testQuickFix(""" + \begin{document} + \section{} + \subsubsection{} + \end{document} + """.trimIndent(), """ + \begin{document} + \section{} + \subsection{} + \subsubsection{} + \end{document} + """.trimIndent()) + } + + @Test + fun `test document starting with subsection warning`() { + testQuickFix(""" + \begin{document} + \subsection{} + \end{document} + """.trimIndent(), """ + \begin{document} + \section{} + \subsection{} + \end{document} + """.trimIndent()) + } + + @Test + fun `test document starting with subparagraph warning`() { + testQuickFix(""" + \begin{document} + \subparagraph{} + \end{document} + """.trimIndent(), """ + \begin{document} + \paragraph{} + \subparagraph{} + \end{document} + """.trimIndent()) + } + + @Test + fun `test subparagraph after section warning`() { + testQuickFix(""" + \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(true, false, false, false) + } + + @Test + fun `test no warning on correct nesting`() { + myFixture.configureByText(LatexFileType, """ + \begin{document} + \section{} + \section{} + \subsection{} + \subsubsection{} + \subsubsection{} + \section{} + \paragraph{} + \paragraph{} + \subparagraph{} + \subparagraph{} + \end{document} + """.trimIndent()) + myFixture.checkHighlighting(true, false, false, false) + } + + private fun testQuickFix(before: String, after: String) { + myFixture.configureByText(LatexFileType, before) + val quickFixes = myFixture.getAllQuickFixes() + writeCommand(myFixture.project) { + quickFixes.first().invoke(myFixture.project, myFixture.editor, myFixture.file) + } + + myFixture.checkResult(after) + } +} \ No newline at end of file From b1a7d7001e82292027693253d4eb36a90b256690 Mon Sep 17 00:00:00 2001 From: johannesberger Date: Sun, 21 Jun 2020 13:25:12 +0200 Subject: [PATCH 2/5] Review comments --- .../LatexIncorrectSectionNestingInspection.kt | 31 ++++++++++--- ...exIncorrectSectionNestingInspectionTest.kt | 43 +++++++++++++++---- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt index 9223ab831..1397637f6 100644 --- a/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt +++ b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt @@ -14,6 +14,7 @@ import nl.hannahsten.texifyidea.util.Magic 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 @@ -31,10 +32,17 @@ open class LatexIncorrectSectionNestingInspection : TexifyInspectionBase() { val sectionCommands = LatexCommandsIndex.getCommandsByNames(file, "\\section", "\\subsection", "\\subsubsection", "\\paragraph", "\\subparagraph").sortedBy { it.textOffset } sectionCommands.forEachIndexed { index, command -> - if (startsWithSubCommand(command, index) - || subsubsectionAftersection(command, sectionCommands, index) - || subParagraphWithoutParagraph(command, sectionCommands, index)) { - descriptors.add(manager.createProblemDescriptor(command, "Incorrect nesting", InsertParentCommandFix(), ProblemHighlightType.WARNING, isOntheFly)) + if (startsWithSubCommand(command, index) || + subsubsectionAfterSection(command, sectionCommands, index) || + subParagraphWithoutParagraph(command, sectionCommands, index)) { + + descriptors.add(manager.createProblemDescriptor(command, + "Incorrect nesting", + arrayOf(InsertParentCommandFix(), ChangeToParentCommandFix()), + ProblemHighlightType.WEAK_WARNING, + isOntheFly, + false) + ) } } return descriptors @@ -45,7 +53,7 @@ open class LatexIncorrectSectionNestingInspection : TexifyInspectionBase() { return ((level == 2 || level == 3 || level == 5) && index == 0) } - private fun subsubsectionAftersection(command: LatexCommands, sectionCommands: List, index: Int) = + private fun subsubsectionAfterSection(command: LatexCommands, sectionCommands: List, index: Int) = command.level() == 3 && sectionCommands[index - 1].level() == 1 private fun subParagraphWithoutParagraph(command: LatexCommands, sectionCommands: List, index: Int) = @@ -72,9 +80,20 @@ open class LatexIncorrectSectionNestingInspection : TexifyInspectionBase() { 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) + } + } } diff --git a/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt b/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt index 314db38b2..0911710c0 100644 --- a/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt +++ b/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt @@ -14,7 +14,7 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase @Test fun `test document missing subsection warning`() { - testQuickFix(""" + testInsertMissingParentCommandQuickFix(""" \begin{document} \section{} \subsubsection{} @@ -28,9 +28,24 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase """.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 document starting with subsection warning`() { - testQuickFix(""" + testInsertMissingParentCommandQuickFix(""" \begin{document} \subsection{} \end{document} @@ -44,7 +59,7 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase @Test fun `test document starting with subparagraph warning`() { - testQuickFix(""" + testInsertMissingParentCommandQuickFix(""" \begin{document} \subparagraph{} \end{document} @@ -58,7 +73,7 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase @Test fun `test subparagraph after section warning`() { - testQuickFix(""" + testInsertMissingParentCommandQuickFix(""" \begin{document} \section{} \subparagraph{} @@ -77,10 +92,10 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase myFixture.configureByText(LatexFileType, """ \begin{document} \section{} - \subsubsection{} + \subsubsection{} \end{document} """.trimIndent()) - myFixture.checkHighlighting(true, false, false, false) + myFixture.checkHighlighting(false, false, true, false) } @Test @@ -99,14 +114,24 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase \subparagraph{} \end{document} """.trimIndent()) - myFixture.checkHighlighting(true, false, false, false) + 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 testQuickFix(before: String, after: String) { + private fun testChangeToParentCommandQuickFix(before: String, after: String) { myFixture.configureByText(LatexFileType, before) val quickFixes = myFixture.getAllQuickFixes() writeCommand(myFixture.project) { - quickFixes.first().invoke(myFixture.project, myFixture.editor, myFixture.file) + quickFixes.first { it.familyName == "Change to parent command" }.invoke(myFixture.project, myFixture.editor, myFixture.file) } myFixture.checkResult(after) From 3307180cbc5606886363b6cc34ed35cf13c400dd Mon Sep 17 00:00:00 2001 From: johannesberger Date: Sun, 21 Jun 2020 13:33:30 +0200 Subject: [PATCH 3/5] Deleted blank line to be compliant with linter --- .../inspections/latex/LatexIncorrectSectionNestingInspection.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt index 1397637f6..15894d2a8 100644 --- a/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt +++ b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt @@ -96,4 +96,3 @@ open class LatexIncorrectSectionNestingInspection : TexifyInspectionBase() { } } } - From bda13faa02a82143c1de776e833e6ea3761f6b6d Mon Sep 17 00:00:00 2001 From: johannesberger Date: Tue, 23 Jun 2020 22:58:28 +0200 Subject: [PATCH 4/5] Now including checks for \part and \chapter but getting rid of start_file_with_sub_command inspection --- .../LatexIncorrectSectionNestingInspection.kt | 60 ++++++++----------- ...exIncorrectSectionNestingInspectionTest.kt | 34 ++++------- 2 files changed, 39 insertions(+), 55 deletions(-) diff --git a/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt index 15894d2a8..b4192b377 100644 --- a/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt +++ b/src/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspection.kt @@ -10,7 +10,6 @@ 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.Magic import nl.hannahsten.texifyidea.util.files.document import nl.hannahsten.texifyidea.util.files.openedEditor import nl.hannahsten.texifyidea.util.lineIndentation @@ -28,43 +27,23 @@ open class LatexIncorrectSectionNestingInspection : TexifyInspectionBase() { override fun getDisplayName() = "Incorrect nesting" override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List { - val descriptors = descriptorList() - - val sectionCommands = LatexCommandsIndex.getCommandsByNames(file, "\\section", "\\subsection", "\\subsubsection", "\\paragraph", "\\subparagraph").sortedBy { it.textOffset } - sectionCommands.forEachIndexed { index, command -> - if (startsWithSubCommand(command, index) || - subsubsectionAfterSection(command, sectionCommands, index) || - subParagraphWithoutParagraph(command, sectionCommands, index)) { - - descriptors.add(manager.createProblemDescriptor(command, - "Incorrect nesting", - arrayOf(InsertParentCommandFix(), ChangeToParentCommandFix()), - ProblemHighlightType.WEAK_WARNING, - isOntheFly, - false) - ) - } - } - return descriptors - } - private fun startsWithSubCommand(command: LatexCommands, index: Int): Boolean { - val level = command.level() - return ((level == 2 || level == 3 || level == 5) && index == 0) + 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 subsubsectionAfterSection(command: LatexCommands, sectionCommands: List, index: Int) = - command.level() == 3 && sectionCommands[index - 1].level() == 1 - - private fun subParagraphWithoutParagraph(command: LatexCommands, sectionCommands: List, index: Int) = - (command.level() == 5 && sectionCommands[index - 1].level() < 4) + private fun sectioningCommands() = commandToForbiddenPredecessors.keys.toTypedArray() - private fun LatexCommands.level(): Int { - return Magic.Command.labeledLevels - .filterKeys { it.command == this.commandToken.text.removePrefix("\\") } - .map { it.value } - .firstOrNull() ?: error("Unexpected command") - } + private fun LatexCommands.commandName(): String = this.commandToken.text private class InsertParentCommandFix : LocalQuickFix { @@ -95,4 +74,17 @@ open class LatexIncorrectSectionNestingInspection : TexifyInspectionBase() { 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 index 0911710c0..af3c0478f 100644 --- a/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt +++ b/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt @@ -29,28 +29,15 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase } @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 document starting with subsection warning`() { + fun `test subsection after chapter warning`() { testInsertMissingParentCommandQuickFix(""" \begin{document} + \chapter{} \subsection{} \end{document} """.trimIndent(), """ \begin{document} + \chapter{} \section{} \subsection{} \end{document} @@ -58,19 +45,21 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase } @Test - fun `test document starting with subparagraph warning`() { - testInsertMissingParentCommandQuickFix(""" + fun `test change subsubsection to subsection quick fix`() { + testChangeToParentCommandQuickFix(""" \begin{document} - \subparagraph{} + \section{} + \subsubsection{} \end{document} """.trimIndent(), """ \begin{document} - \paragraph{} - \subparagraph{} + \section{} + \subsection{} \end{document} """.trimIndent()) } + @Test fun `test subparagraph after section warning`() { testInsertMissingParentCommandQuickFix(""" @@ -102,6 +91,9 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase fun `test no warning on correct nesting`() { myFixture.configureByText(LatexFileType, """ \begin{document} + \part{} + \part{} + \chapter{} \section{} \section{} \subsection{} From 38f20e60cd6a1d3284dd925a7784b258d9b75a2a Mon Sep 17 00:00:00 2001 From: johannesberger Date: Tue, 23 Jun 2020 23:04:27 +0200 Subject: [PATCH 5/5] Making the linter happy --- .../latex/LatexIncorrectSectionNestingInspectionTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt b/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt index af3c0478f..c01bb978b 100644 --- a/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt +++ b/test/nl/hannahsten/texifyidea/inspections/latex/LatexIncorrectSectionNestingInspectionTest.kt @@ -59,7 +59,6 @@ internal class LatexIncorrectSectionNestingInspectionTest : BasePlatformTestCase """.trimIndent()) } - @Test fun `test subparagraph after section warning`() { testInsertMissingParentCommandQuickFix("""