diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ccc4155..e991ba3f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,21 @@ ## [Unreleased] ### Added + +### Fixed + +## [0.9.3-alpha.3] - 2024-01-08 + +### Added + * Improve plugin loading performance ### Fixed + +* Avoid creating output directories recursively and improve the cleanup process +* Don't attempt to use mthelp when it is not available, by @jojo2357 * Fix #3361: false positive on duplicate identifier on @string entries in bib files +* Replace code deprecated in 2023.3 ## [0.9.2] - 2023-11-24 @@ -257,7 +268,8 @@ Thanks to @jojo2357 and @MisterDeenis for contributing to this release! * Fix some intention previews. ([#2796](https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/2796)) * Other small bug fixes and improvements. ([#2776](https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/2776), [#2774](https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/2774), [#2765](https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/2765)-[#2773](https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/2773)) -[Unreleased]: https://github.com/Hannah-Sten/TeXiFy-IDEA/compare/v0.9.2...HEAD +[Unreleased]: https://github.com/Hannah-Sten/TeXiFy-IDEA/compare/v0.9.3-alpha.3...HEAD +[0.9.3-alpha.3]: https://github.com/Hannah-Sten/TeXiFy-IDEA/compare/v0.9.2...v0.9.3-alpha.3 [0.9.2]: https://github.com/Hannah-Sten/TeXiFy-IDEA/compare/v0.9.1...v0.9.2 [0.9.1]: https://github.com/Hannah-Sten/TeXiFy-IDEA/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/Hannah-Sten/TeXiFy-IDEA/compare/v0.7.33...v0.9.0 diff --git a/build.gradle.kts b/build.gradle.kts index 1b0fac393..00cc3952e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,7 +29,7 @@ plugins { id("org.jlleitschuh.gradle.ktlint") version "11.3.2" // Vulnerability scanning - id("org.owasp.dependencycheck") version "9.0.7" + id("org.owasp.dependencycheck") version "9.0.8" id("org.jetbrains.changelog") version "2.2.0" @@ -84,7 +84,7 @@ dependencies { // D-Bus Java bindings implementation("com.github.hypfvieh:dbus-java:3.3.2") - implementation("org.slf4j:slf4j-simple:2.0.10") + implementation("org.slf4j:slf4j-simple:2.0.11") // Unzipping tar.xz/tar.bz2 files on Windows containing dtx files implementation("org.codehaus.plexus:plexus-component-api:1.0-alpha-33") @@ -138,7 +138,7 @@ dependencies { // Enable use of the JUnitPlatform Runner within the IDE testImplementation("org.junit.platform:junit-platform-runner:1.10.1") - testImplementation("io.mockk:mockk:1.13.8") + testImplementation("io.mockk:mockk:1.13.9") // Add custom ruleset from github.com/slideclimb/ktlint-ruleset ktlintRuleset(files("lib/ktlint-ruleset-0.2.jar")) diff --git a/gradle.properties b/gradle.properties index 14af5be08..2a85fdb8e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -pluginVersion = 0.9.3-alpha.2 +pluginVersion = 0.9.3-alpha.3 # Info about build ranges: https://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html # Note that an xyz branch corresponds to version 20xy.z and a since build of xyz.* diff --git a/src/nl/hannahsten/texifyidea/completion/LatexCommandsAndEnvironmentsCompletionProvider.kt b/src/nl/hannahsten/texifyidea/completion/LatexCommandsAndEnvironmentsCompletionProvider.kt index 9b805c9d5..8b444658d 100644 --- a/src/nl/hannahsten/texifyidea/completion/LatexCommandsAndEnvironmentsCompletionProvider.kt +++ b/src/nl/hannahsten/texifyidea/completion/LatexCommandsAndEnvironmentsCompletionProvider.kt @@ -48,6 +48,22 @@ class LatexCommandsAndEnvironmentsCompletionProvider internal constructor(privat /** Whether TeX Live is available at all, in which case it could be that all packages from texlive-full are in the index. */ val isTexliveAvailable = TexliveSdk.Cache.isAvailable || ProjectJdkTable.getInstance().allJdks.any { it.sdkType is TexliveSdk } + + fun createCommandLookupElements(cmd: LatexCommand): List { + return cmd.arguments.toSet().optionalPowerSet().mapIndexed { index, args -> + // Add spaces to the lookup text to distinguish different versions of commands within the same package (optional parameters). + // Add the package name to the lookup text so we can distinguish between the same commands that come from different packages. + // This 'extra' text will be automatically inserted by intellij and is removed by the LatexCommandArgumentInsertHandler after insertion. + val default = cmd.dependency.isDefault + LookupElementBuilder.create(cmd, cmd.command + " ".repeat(index + default.not().int) + cmd.dependency.displayString) + .withPresentableText(cmd.commandWithSlash) + .bold() + .withTailText(args.joinToString("") + " " + packageName(cmd), true) + .withTypeText(cmd.display) + .withInsertHandler(LatexNoMathInsertHandler(args.toList())) + .withIcon(TexifyIcons.DOT_COMMAND) + } + } } override fun addCompletions( @@ -57,8 +73,11 @@ class LatexCommandsAndEnvironmentsCompletionProvider internal constructor(privat ) { when (mode) { LatexMode.NORMAL -> { - addIndexedCommands(result, parameters) - addNormalCommands(result, parameters.editor.project ?: return) + // This can be really slow (one minute), so we don't wait until the cache is filled + val isIndexReady = addIndexedCommands(result, parameters) + // This should be quite fast (less than a second) + addNormalCommands(result, parameters.editor.project ?: return, isIndexReady) + // Filling the cache can take two seconds, for now we wait on it addCustomCommands(parameters, result) } LatexMode.MATH -> { @@ -73,74 +92,37 @@ class LatexCommandsAndEnvironmentsCompletionProvider internal constructor(privat result.addLookupAdvertisement("Don't use \\\\ outside of tabular or math mode, it's evil.") } - private fun createCommandLookupElements(cmd: LatexCommand): List { - return cmd.arguments.toSet().optionalPowerSet().mapIndexed { index, args -> - // Add spaces to the lookup text to distinguish different versions of commands within the same package (optional parameters). - // Add the package name to the lookup text so we can distinguish between the same commands that come from different packages. - // This 'extra' text will be automatically inserted by intellij and is removed by the LatexCommandArgumentInsertHandler after insertion. - val default = cmd.dependency.isDefault - LookupElementBuilder.create(cmd, cmd.command + " ".repeat(index + default.not().int) + cmd.dependency.displayString) - .withPresentableText(cmd.commandWithSlash) - .bold() - .withTailText(args.joinToString("") + " " + packageName(cmd), true) - .withTypeText(cmd.display) - .withInsertHandler(LatexNoMathInsertHandler(args.toList())) - .withIcon(TexifyIcons.DOT_COMMAND) - } - } - /** * Add all indexed commands to the autocompletion. + * This is a potentially long-running operation because it needs to process all keys in the index, hence it uses a cache. + * + * @return If any commands were added. */ - private fun addIndexedCommands(result: CompletionResultSet, parameters: CompletionParameters) { - // Use cache if available - if (indexedCommands.isNotEmpty()) { - result.addAllElements(indexedCommands) - return - } - val project = parameters.editor.project ?: return - - // If using texlive, filter on commands which are in packages included in the project - // The reason for doing this, is that the user probably is using texlive-full, in which case the - // completion would be flooded with duplicate commands from packages that nobody uses. - // For example, the (initially) first suggestion for \enquote is the version from the aiaa package, which is unlikely to be correct. - // Therefore, we limit ourselves to packages included somewhere in the project (directly or indirectly). - val packagesInProject = if (!isTexliveAvailable) emptyList() else includedPackages(LatexIncludesIndex.Util.getItems(project), project).plus(LatexPackage.DEFAULT) - - val lookupElementBuilders = mutableSetOf() + private fun addIndexedCommands(result: CompletionResultSet, parameters: CompletionParameters): Boolean { + val lookupElementBuilders = LatexExternalCommandsIndexCache.completionElements + if (lookupElementBuilders.isEmpty()) { + // Filling the cache can take a long time, and since we don't _really_ need the results right now and nobody is going to wait 60 seconds for autocompletion to pop up, we just trigger a cache fill and move on + val project = parameters.editor.project ?: return false - val commandsFromIndex = mutableListOf>() - - FileBasedIndex.getInstance().processAllKeys( - LatexExternalCommandIndex.Cache.id, - { cmdWithSlash -> commandsFromIndex.add(LatexCommand.lookupInIndex(cmdWithSlash.substring(1), project)) }, - GlobalSearchScope.everythingScope(project), - null - ) - - // Process each set of command aliases (commands with the same name, but possibly with different arguments) separately. - commandsFromIndex.map { commandAliases -> - commandAliases.filter { command -> if (isTexliveAvailable) command.dependency in packagesInProject else true } - .forEach { cmd -> - createCommandLookupElements(cmd) - // Avoid duplicates of commands defined in LaTeX base, because they are often very similar commands defined in different document classes, so it makes not - // much sense at the moment to have them separately in the autocompletion. - // Effectively this results in just taking the first one we found - .filter { newBuilder -> - if (cmd.dependency.isDefault) { - lookupElementBuilders.none { it.lookupString == newBuilder.lookupString } - } - else { - true - } - }.forEach { lookupElementBuilders.add(it) } - } + // If using texlive, filter on commands which are in packages included in the project + // The reason for doing this, is that the user probably is using texlive-full, in which case the + // completion would be flooded with duplicate commands from packages that nobody uses. + // For example, the (initially) first suggestion for \enquote is the version from the aiaa package, which is unlikely to be correct. + // Therefore, we limit ourselves to packages included somewhere in the project (directly or indirectly). + val packagesInProject = if (!isTexliveAvailable) emptyList() else includedPackages(LatexIncludesIndex.Util.getItems(project), project).plus(LatexPackage.DEFAULT) + LatexExternalCommandsIndexCache.fillCacheAsync(project, packagesInProject) + return false } - indexedCommands.addAll(lookupElementBuilders) + result.addAllElements(lookupElementBuilders) + return true } - private fun addNormalCommands(result: CompletionResultSet, project: Project) { + /** + * Add any commands that were not found in the indexed commands but are hardcoded in LatexRegularCommand. + * If the index was not yet ready, add all of them. + */ + private fun addNormalCommands(result: CompletionResultSet, project: Project, isIndexReady: Boolean) { val indexedKeys = FileBasedIndex.getInstance().getAllKeys(LatexExternalCommandIndex.Cache.id, project) result.addAllElements( @@ -152,7 +134,7 @@ class LatexCommandsAndEnvironmentsCompletionProvider internal constructor(privat // Avoid adding duplicates // Prefer the indexed command (if it really is the same one), as that one has documentation - if (cmd.commandWithSlash in indexedKeys && alreadyIndexed()) { + if (isIndexReady && cmd.commandWithSlash in indexedKeys && alreadyIndexed()) { emptyList() } else { diff --git a/src/nl/hannahsten/texifyidea/completion/LatexExternalCommandsIndexCache.kt b/src/nl/hannahsten/texifyidea/completion/LatexExternalCommandsIndexCache.kt new file mode 100644 index 000000000..b3d25234d --- /dev/null +++ b/src/nl/hannahsten/texifyidea/completion/LatexExternalCommandsIndexCache.kt @@ -0,0 +1,117 @@ +package nl.hannahsten.texifyidea.completion + +import arrow.atomic.AtomicBoolean +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.application.runReadAction +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.search.GlobalSearchScope +import com.intellij.util.indexing.FileBasedIndex +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import nl.hannahsten.texifyidea.completion.LatexCommandsAndEnvironmentsCompletionProvider.Companion.createCommandLookupElements +import nl.hannahsten.texifyidea.index.file.LatexExternalCommandIndex +import nl.hannahsten.texifyidea.lang.LatexPackage +import nl.hannahsten.texifyidea.lang.commands.LatexCommand + +/** + * Cache for [LatexExternalCommandIndex], as index access is very expensive. + * This cache will not be updated while the IDE is running. + */ +object LatexExternalCommandsIndexCache { + private val scope = CoroutineScope(Dispatchers.Default) + + private val isCacheFillInProgress = AtomicBoolean(false) + + var completionElements = setOf() + + fun unload() { + scope.cancel() + } + + /** + * Initiate a cache fill but do not wait for it to be filled. + */ + fun fillCacheAsync(project: Project, packagesInProject: List) { + if (isCacheFillInProgress.compareAndSet(expected = true, new = true)) { + return + } + isCacheFillInProgress.getAndSet(true) + + scope.launch { + ProgressManager.getInstance().run(object : Backgroundable(project, "Retrieving LaTeX commands...") { + override fun run(indicator: ProgressIndicator) { + try { + val commandsFromIndex = getIndexedCommandsNoCache(project, indicator) + completionElements = createLookupElements(commandsFromIndex, packagesInProject, indicator) + } + finally { + isCacheFillInProgress.getAndSet(false) + } + } + }) + } + } + + /** + * This may be a very expensive operation, up to one minute for texlive-full + */ + private fun getIndexedCommandsNoCache(project: Project, indicator: ProgressIndicator): MutableList> { + indicator.text = "Getting commands from index..." + val commands = mutableListOf() + runReadAction { + FileBasedIndex.getInstance().processAllKeys( + LatexExternalCommandIndex.Cache.id, + { cmdWithSlash -> commands.add(cmdWithSlash) }, + GlobalSearchScope.everythingScope(project), + null + ) + } + + indicator.text = "Processing indexed commands..." + val commandsFromIndex = mutableListOf>() + for ((index, cmdWithSlash) in commands.withIndex()) { + indicator.checkCanceled() + indicator.fraction = index.toDouble() / commands.size + commandsFromIndex.add(LatexCommand.lookupInIndex(cmdWithSlash.substring(1), project)) + } + + return commandsFromIndex + } + + private fun createLookupElements( + commandsFromIndex: MutableList>, + packagesInProject: List, + indicator: ProgressIndicator + ): MutableSet { + indicator.text = "Adding commands to autocompletion..." + val lookupElementBuilders = mutableSetOf() + + // Process each set of command aliases (commands with the same name, but possibly with different arguments) separately. + commandsFromIndex.mapIndexed { index, commandAliases -> + indicator.fraction = index.toDouble() / commandsFromIndex.size + indicator.checkCanceled() + commandAliases.filter { command -> if (LatexCommandsAndEnvironmentsCompletionProvider.isTexliveAvailable) command.dependency in packagesInProject else true } + .forEach { cmd -> + createCommandLookupElements(cmd) + // Avoid duplicates of commands defined in LaTeX base, because they are often very similar commands defined in different document classes, so it makes not + // much sense at the moment to have them separately in the autocompletion. + // Effectively this results in just taking the first one we found + .filter { newBuilder -> + if (cmd.dependency.isDefault) { + lookupElementBuilders.none { it.lookupString == newBuilder.lookupString } + } + else { + true + } + }.forEach { lookupElementBuilders.add(it) } + } + } + + return lookupElementBuilders + } +} \ No newline at end of file diff --git a/src/nl/hannahsten/texifyidea/index/IndexUtilBase.kt b/src/nl/hannahsten/texifyidea/index/IndexUtilBase.kt index 0c0b93de8..e39355e5b 100644 --- a/src/nl/hannahsten/texifyidea/index/IndexUtilBase.kt +++ b/src/nl/hannahsten/texifyidea/index/IndexUtilBase.kt @@ -1,6 +1,7 @@ package nl.hannahsten.texifyidea.index import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement @@ -142,6 +143,9 @@ abstract class IndexUtilBase( catch (e: Exception) { // For some reason, any issue from any plugin that causes an exception will be raised here and will be attributed to TeXiFy, flooding the backlog // Hence, we just ignore all of them and hope it's not important + if (e is ProcessCanceledException) { + throw e + } Log.warn(e.toString()) } return emptySet() @@ -160,6 +164,9 @@ abstract class IndexUtilBase( } catch (e: Exception) { // See above + if (e is ProcessCanceledException) { + throw e + } Log.warn(e.toString()) } } diff --git a/src/nl/hannahsten/texifyidea/lang/commands/LatexCommand.kt b/src/nl/hannahsten/texifyidea/lang/commands/LatexCommand.kt index b70e003ec..eecd30eb9 100644 --- a/src/nl/hannahsten/texifyidea/lang/commands/LatexCommand.kt +++ b/src/nl/hannahsten/texifyidea/lang/commands/LatexCommand.kt @@ -1,8 +1,10 @@ package nl.hannahsten.texifyidea.lang.commands import arrow.core.NonEmptySet +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.indexing.FileBasedIndex import kotlinx.coroutines.runBlocking @@ -12,8 +14,8 @@ import nl.hannahsten.texifyidea.lang.Described import nl.hannahsten.texifyidea.lang.LatexPackage import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.* import nl.hannahsten.texifyidea.psi.LatexCommands -import nl.hannahsten.texifyidea.util.parser.inMathContext import nl.hannahsten.texifyidea.util.length +import nl.hannahsten.texifyidea.util.parser.inMathContext import nl.hannahsten.texifyidea.util.startsWithAny import kotlin.reflect.KClass @@ -52,8 +54,18 @@ interface LatexCommand : Described, Dependend { val cmds = lookup(cmdWithSlash)?.toMutableSet() ?: mutableSetOf() // Look up in index - FileBasedIndex.getInstance().processValues( - LatexExternalCommandIndex.Cache.id, cmdWithSlash, null, { file, value -> + val filesAndValues = mutableListOf>() + runReadAction { + FileBasedIndex.getInstance().processValues( + LatexExternalCommandIndex.Cache.id, cmdWithSlash, null, { file, value -> + filesAndValues.add(Pair(file, value)) + true + }, + GlobalSearchScope.everythingScope(project) + ) + } + + for ((file, value) in filesAndValues) { val dependency = LatexPackage.create(file) // Merge with already known command if possible, assuming that there was a reason to specify things (especially parameters) manually // Basically this means we add the indexed docs to the known command @@ -80,10 +92,7 @@ interface LatexCommand : Described, Dependend { } } cmds.add(cmd) - true - }, - GlobalSearchScope.everythingScope(project) - ) + } // Now we might have duplicates, some of which might differ only in description. // Of those, we just want to take any command which doesn't have an empty description if it exists diff --git a/src/nl/hannahsten/texifyidea/startup/LoadUnloadListener.kt b/src/nl/hannahsten/texifyidea/startup/LoadUnloadListener.kt index 7af6911b9..1f355d86f 100644 --- a/src/nl/hannahsten/texifyidea/startup/LoadUnloadListener.kt +++ b/src/nl/hannahsten/texifyidea/startup/LoadUnloadListener.kt @@ -3,6 +3,7 @@ package nl.hannahsten.texifyidea.startup import com.intellij.ide.plugins.DynamicPluginListener import com.intellij.ide.plugins.IdeaPluginDescriptor import kotlinx.coroutines.runBlocking +import nl.hannahsten.texifyidea.completion.LatexExternalCommandsIndexCache import nl.hannahsten.texifyidea.run.linuxpdfviewer.evince.EvinceInverseSearchListener class LoadUnloadListener : DynamicPluginListener { @@ -12,6 +13,7 @@ class LoadUnloadListener : DynamicPluginListener { // ControlTracker.unload() // ShiftTracker.unload() runBlocking { EvinceInverseSearchListener.unload() } + LatexExternalCommandsIndexCache.unload() super.beforePluginUnload(pluginDescriptor, isUpdate) } } \ No newline at end of file