Skip to content

Commit

Permalink
Merge branch 'master' into preview-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
PHPirates committed Jan 12, 2024
2 parents 6d5889e + 930896c commit 6c3351a
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 81 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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"))
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -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.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<LookupElementBuilder> {
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(
Expand All @@ -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 -> {
Expand All @@ -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<LookupElementBuilder> {
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<LookupElementBuilder>()
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<Set<LatexCommand>>()

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(
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LookupElementBuilder>()

fun unload() {
scope.cancel()
}

/**
* Initiate a cache fill but do not wait for it to be filled.
*/
fun fillCacheAsync(project: Project, packagesInProject: List<LatexPackage>) {
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<Set<LatexCommand>> {
indicator.text = "Getting commands from index..."
val commands = mutableListOf<String>()
runReadAction {
FileBasedIndex.getInstance().processAllKeys(
LatexExternalCommandIndex.Cache.id,
{ cmdWithSlash -> commands.add(cmdWithSlash) },
GlobalSearchScope.everythingScope(project),
null
)
}

indicator.text = "Processing indexed commands..."
val commandsFromIndex = mutableListOf<Set<LatexCommand>>()
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<Set<LatexCommand>>,
packagesInProject: List<LatexPackage>,
indicator: ProgressIndicator
): MutableSet<LookupElementBuilder> {
indicator.text = "Adding commands to autocompletion..."
val lookupElementBuilders = mutableSetOf<LookupElementBuilder>()

// 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
}
}
Loading

0 comments on commit 6c3351a

Please sign in to comment.