diff --git a/CHANGELOG.md b/CHANGELOG.md index 866e8dd62..72841345d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * 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 inspection to check for LaTeX package updates * Add checkboxes to graphic insertion wizard for relative width or height ### Fixed diff --git a/Writerside/topics/Packages.md b/Writerside/topics/Packages.md index b6ddc6e02..d5eaf019a 100644 --- a/Writerside/topics/Packages.md +++ b/Writerside/topics/Packages.md @@ -13,6 +13,13 @@ This inspection is for TeX Live only, MiKTeX automatically installs packages on When using `\usepackage` or `\RequirePackage`, TeXiFy checks if the packages is installed. If it isn’t installed, it provides a quick fix to install the package. +## Package update available +_Since b0.9.10_ + +When a package has an update available on CTAN, this inspection will provide a quickfix to update the package. +Currently, it only works when tlmgr (TeX Live manager) is installed. +The list of available package updates is cached until IntelliJ is restarted or the quickfix is used. + ## Package name does not match file name _Since b0.6.10_ diff --git a/resources/META-INF/extensions/inspections/latex/probablebugs/packages.xml b/resources/META-INF/extensions/inspections/latex/probablebugs/packages.xml index 9f46f9a2b..db654fe87 100644 --- a/resources/META-INF/extensions/inspections/latex/probablebugs/packages.xml +++ b/resources/META-INF/extensions/inspections/latex/probablebugs/packages.xml @@ -8,6 +8,10 @@ groupPath="LaTeX" groupName="Probable bugs" displayName="Package is not installed" enabledByDefault="true" level="WARNING" /> + - \ No newline at end of file diff --git a/resources/inspectionDescriptions/LatexPackageUpdate.html b/resources/inspectionDescriptions/LatexPackageUpdate.html new file mode 100644 index 000000000..5fd73b15b --- /dev/null +++ b/resources/inspectionDescriptions/LatexPackageUpdate.html @@ -0,0 +1,8 @@ + + + +The package given in a \usepackage or \RequirePackage has an update available. + +This inspection only checks installed packages on texlive systems (with tlmgr). + + \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageNotInstalledInspection.kt b/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageNotInstalledInspection.kt index be2869d0d..9f9902f04 100644 --- a/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageNotInstalledInspection.kt +++ b/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageNotInstalledInspection.kt @@ -1,6 +1,7 @@ package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo import com.intellij.codeInspection.InspectionManager import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor @@ -17,10 +18,13 @@ import com.intellij.psi.SmartPsiElementPointer import nl.hannahsten.texifyidea.index.LatexDefinitionIndex import nl.hannahsten.texifyidea.inspections.InsightGroup import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase +import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand import nl.hannahsten.texifyidea.psi.LatexCommands import nl.hannahsten.texifyidea.reference.InputFileReference import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil +import nl.hannahsten.texifyidea.settings.sdk.TexliveSdk import nl.hannahsten.texifyidea.util.TexLivePackages +import nl.hannahsten.texifyidea.util.magic.cmd import nl.hannahsten.texifyidea.util.parser.childrenOfType import nl.hannahsten.texifyidea.util.parser.requiredParameter import nl.hannahsten.texifyidea.util.projectSearchScope @@ -49,48 +53,54 @@ class LatexPackageNotInstalledInspection : TexifyInspectionBase() { override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List { val descriptors = descriptorList() // We have to check whether tlmgr is installed, for those users who don't want to install TeX Live in the official way - if (LatexSdkUtil.isTlmgrAvailable(file.project)) { - val installedPackages = TexLivePackages.packageList - val customPackages = LatexDefinitionIndex.Util.getCommandsByName( - "\\ProvidesPackage", file.project, - file.project - .projectSearchScope - ) - .map { it.requiredParameter(0) } - .mapNotNull { it?.lowercase(Locale.getDefault()) } - val packages = installedPackages + customPackages - - val commands = file.childrenOfType(LatexCommands::class) - .filter { it.name == "\\usepackage" || it.name == "\\RequirePackage" } - - for (command in commands) { - @Suppress("ktlint:standard:property-naming") - val `package` = command.getRequiredParameters().firstOrNull()?.lowercase(Locale.getDefault()) ?: continue - if (`package` !in packages) { - // Use the cache or check if the file reference resolves (in the same way we resolve for the gutter icon). - if ( - knownNotInstalledPackages.contains(`package`) || - command.references.filterIsInstance().mapNotNull { it.resolve() }.isEmpty() - ) { - descriptors.add( - manager.createProblemDescriptor( - command, - "Package is not installed or \\ProvidesPackage is missing", - InstallPackage( - SmartPointerManager.getInstance(file.project).createSmartPsiElementPointer(file), - `package`, - knownNotInstalledPackages - ), - ProblemHighlightType.GENERIC_ERROR_OR_WARNING, - isOntheFly - ) + if (!LatexSdkUtil.isTlmgrAvailable(file.project)) return descriptors + + if (TexLivePackages.packageList.isEmpty() && TexliveSdk.Cache.isAvailable) { + val result = "tlmgr list --only-installed".runCommand() ?: return emptyList() + TexLivePackages.packageList = Regex("i\\s(.*):").findAll(result) + .map { it.groupValues.last() }.toMutableList() + } + + val installedPackages = TexLivePackages.packageList + val customPackages = LatexDefinitionIndex.Util.getCommandsByName( + LatexGenericRegularCommand.PROVIDESPACKAGE.cmd, file.project, + file.project + .projectSearchScope + ) + .map { it.requiredParameter(0) } + .mapNotNull { it?.lowercase(Locale.getDefault()) } + val packages = installedPackages + customPackages + + val commands = file.childrenOfType(LatexCommands::class) + .filter { it.name == LatexGenericRegularCommand.USEPACKAGE.cmd || it.name == LatexGenericRegularCommand.REQUIREPACKAGE.cmd } + + for (command in commands) { + @Suppress("ktlint:standard:property-naming") + val `package` = command.getRequiredParameters().firstOrNull()?.lowercase(Locale.getDefault()) ?: continue + if (`package` !in packages) { + // Use the cache or check if the file reference resolves (in the same way we resolve for the gutter icon). + if ( + knownNotInstalledPackages.contains(`package`) || + command.references.filterIsInstance().mapNotNull { it.resolve() }.isEmpty() + ) { + descriptors.add( + manager.createProblemDescriptor( + command, + "Package is not installed or \\ProvidesPackage is missing", + InstallPackage( + SmartPointerManager.getInstance(file.project).createSmartPsiElementPointer(file), + `package`, + knownNotInstalledPackages + ), + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + isOntheFly ) - knownNotInstalledPackages.add(`package`) - } - else { - // Apparently the package is installed, but was not found initially by the TexLivePackageListInitializer (for example stackrel, contained in the oberdiek bundle) - TexLivePackages.packageList.add(`package`) - } + ) + knownNotInstalledPackages.add(`package`) + } + else { + // Apparently the package is installed, but was not found initially by the TexLivePackageListInitializer (for example stackrel, contained in the oberdiek bundle) + TexLivePackages.packageList.add(`package`) } } } @@ -101,6 +111,11 @@ class LatexPackageNotInstalledInspection : TexifyInspectionBase() { override fun getFamilyName(): String = "Install $packageName" + override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo { + // Nothing is modified + return IntentionPreviewInfo.EMPTY + } + /** * Install the package in the background and add it to the list of installed * packages when done. diff --git a/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageUpdateInspection.kt b/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageUpdateInspection.kt new file mode 100644 index 000000000..10bfc761d --- /dev/null +++ b/src/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageUpdateInspection.kt @@ -0,0 +1,136 @@ +package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.notification.Notification +import com.intellij.notification.NotificationType +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task.Backgroundable +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.SmartPointerManager +import com.intellij.psi.SmartPsiElementPointer +import nl.hannahsten.texifyidea.inspections.InsightGroup +import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase +import nl.hannahsten.texifyidea.psi.LatexCommands +import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil +import nl.hannahsten.texifyidea.settings.sdk.TexliveSdk +import nl.hannahsten.texifyidea.util.magic.CommandMagic +import nl.hannahsten.texifyidea.util.parser.childrenOfType +import nl.hannahsten.texifyidea.util.parser.requiredParameter +import nl.hannahsten.texifyidea.util.runCommand +import nl.hannahsten.texifyidea.util.runCommandWithExitCode + +/** + * Check for available updates for LaTeX packages. + * Also see [LatexPackageNotInstalledInspection]. + */ +class LatexPackageUpdateInspection : TexifyInspectionBase() { + + object Cache { + /** Map package name to old and new revision number */ + var availablePackageUpdates = mapOf>() + } + + override val inspectionGroup = InsightGroup.LATEX + + override val inspectionId = "PackageUpdate" + + override fun getDisplayName() = "Package has an update available" + + override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List { + if (!LatexSdkUtil.isTlmgrAvailable(file.project) || !TexliveSdk.Cache.isAvailable) return emptyList() + + if (Cache.availablePackageUpdates.isEmpty()) { + val tlmgrExecutable = LatexSdkUtil.getExecutableName("tlmgr", file.project) + val result = runCommand(tlmgrExecutable, "update", "--list") ?: return emptyList() + Cache.availablePackageUpdates = """update:\s*(?[^ ]+).*local:\s*(?\d+), source:\s*(?\d+)""".toRegex() + .findAll(result) + .mapNotNull { Pair(it.groups["package"]?.value ?: return@mapNotNull null, Pair(it.groups["local"]?.value, it.groups["source"]?.value)) } + .associate { it } + } + + return file.childrenOfType() + .filter { it.name in CommandMagic.packageInclusionCommands } + .filter { it.requiredParameter(0) in Cache.availablePackageUpdates.keys } + .mapNotNull { + val packageName = it.requiredParameter(0) ?: return@mapNotNull null + val packageVersions = Cache.availablePackageUpdates[packageName] ?: return@mapNotNull null + manager.createProblemDescriptor( + it, + "Update available for package $packageName", + arrayOf( + UpdatePackage(SmartPointerManager.getInstance(file.project).createSmartPsiElementPointer(file), packageName, packageVersions.first, packageVersions.second), + UpdatePackage(SmartPointerManager.getInstance(file.project).createSmartPsiElementPointer(file), "--all", null, null), + ), + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + isOntheFly, + false, + ) + } + } + + private class UpdatePackage(val filePointer: SmartPsiElementPointer, val packageName: String, val old: String?, val new: String?) : LocalQuickFix { + + override fun getFamilyName(): String = if (packageName == "--all") "Update all packages" else if (old != null && new != null) "Update $packageName from revision $old to revision $new" else "Update $packageName" + + override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo { + // Nothing is modified + return IntentionPreviewInfo.Html("Run tlngr update $packageName") + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val message = if (packageName == "--all") "Updating all packages" else "Updating $packageName..." + ProgressManager.getInstance().run(object : Backgroundable(project, message) { + override fun run(indicator: ProgressIndicator) { + val tlmgrExecutable = LatexSdkUtil.getExecutableName("tlmgr", project) + + val timeout: Long = if (packageName == "--all") 1200 else 15 + var (output, exitCode) = runCommandWithExitCode(tlmgrExecutable, "update", packageName, returnExceptionMessage = true, timeout = timeout) + if (output?.contains("tlmgr update --self") == true) { + val (tlmgrOutput, tlmgrExitCode) = runCommandWithExitCode(tlmgrExecutable, "update", "--self", returnExceptionMessage = true, timeout = 20) + if (tlmgrExitCode != 0) { + Notification( + "LaTeX", + "Package $packageName not updated", + "Could not update tlmgr: $tlmgrOutput", + NotificationType.ERROR + ).notify(project) + indicator.cancel() + } + title = message + val (secondOutput, secondExitCode) = runCommandWithExitCode(tlmgrExecutable, "update", packageName, returnExceptionMessage = true, timeout = timeout) + output = secondOutput + exitCode = secondExitCode + } + + if (exitCode != 0) { + Notification( + "LaTeX", + if (packageName == "--all") "Could not update packages" else "Package $packageName not updated", + "Could not update $packageName${if (exitCode == 143) " due to a timeout" else ""}: $output", + NotificationType.ERROR + ).notify(project) + indicator.cancel() + } + } + + override fun onSuccess() { + // Clear cache, since we changed something + Cache.availablePackageUpdates = mapOf() + // Rerun inspections + DaemonCodeAnalyzer.getInstance(project) + .restart( + filePointer.containingFile + ?: return + ) + } + }) + } + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/startup/TexLivePackageListInitializer.kt b/src/nl/hannahsten/texifyidea/startup/TexLivePackageListInitializer.kt deleted file mode 100644 index b23a97a24..000000000 --- a/src/nl/hannahsten/texifyidea/startup/TexLivePackageListInitializer.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nl.hannahsten.texifyidea.startup - -import com.intellij.openapi.project.Project -import com.intellij.openapi.startup.ProjectActivity -import nl.hannahsten.texifyidea.settings.sdk.TexliveSdk -import nl.hannahsten.texifyidea.util.TexLivePackages -import nl.hannahsten.texifyidea.util.runCommandNonBlocking - -class TexLivePackageListInitializer : ProjectActivity { - - override suspend fun execute(project: Project) { - if (TexliveSdk.Cache.isAvailable) { - val result = "tlmgr list --only-installed".runCommandNonBlocking().output ?: return - TexLivePackages.packageList = Regex("i\\s(.*):").findAll(result) - .map { it.groupValues.last() }.toMutableList() - } - } -} \ No newline at end of file diff --git a/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageUpdateInspectionTest.kt b/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageUpdateInspectionTest.kt new file mode 100644 index 000000000..fcf7016cd --- /dev/null +++ b/test/nl/hannahsten/texifyidea/inspections/latex/probablebugs/packages/LatexPackageUpdateInspectionTest.kt @@ -0,0 +1,28 @@ +package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages + +import io.mockk.every +import io.mockk.mockkObject +import nl.hannahsten.texifyidea.inspections.TexifyInspectionTestBase +import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil +import nl.hannahsten.texifyidea.settings.sdk.TexliveSdk +import nl.hannahsten.texifyidea.util.TexLivePackages + +class LatexPackageUpdateInspectionTest : TexifyInspectionTestBase(LatexPackageUpdateInspection()) { + + fun testWarning() { + texliveWithTlmgr() + + mockkObject(TexLivePackages) + LatexPackageUpdateInspection.Cache.availablePackageUpdates = mapOf(Pair("amsmath", Pair("71408", "72779"))) + + testHighlighting("\\usepackage{amsmath}") + } + + private fun texliveWithTlmgr(texlive: Boolean = true, tlmgr: Boolean = true) { + mockkObject(TexliveSdk.Cache) + every { TexliveSdk.Cache.isAvailable } returns texlive + + mockkObject(LatexSdkUtil) + every { LatexSdkUtil.isTlmgrInstalled } returns tlmgr + } +} \ No newline at end of file