### Discussed in https://github.com/Hannah-Sten/TeXiFy-IDEA/discussions/3035

Originally posted by **mvscheven** April 14, 2023
We are using a lot of custom classes and style files to keep the preamble of the main tex-file clean. If these custom files are in the source directory, they are known by TeXiFy and included commands are recognized. But if these files are in a location specified by the TEXINPUTS environment variable this does not work. Is it possible to index also files located at TEXINPUTS?

Thanks for any advice!

### Minimal Example:
#### main.tex
```
\\documentclass[11pt]{scrartcl}
\\RequirePackage{my_style}

\\begin{document}
    \\begin{align}
        \\sigma = E \\varepsilon
    \\end{align}
\\end{document}
```
#### my_style.sty
```
\\RequirePackage{amsmath}
```
If my_style.sty is in the source directory, it is linked in the margin next to the RequirePackage command and align is recognized as valid environment.
If my_style.sty is in a TEXINPUTS directory, it is marked as "not found" and for align an error is produced: "Environment requires amsmath package"

### System:
Windows 11, MikTex, IntelliJ IDEA 2023.1, TexiFy IDEA 0.7.28

diff --git a/src/nl/hannahsten/texifyidea/reference/InputFileReference.kt b/src/nl/hannahsten/texifyidea/reference/InputFileReference.kt
index 65a6a900e..430839b5b 100644
--- a/src/nl/hannahsten/texifyidea/reference/InputFileReference.kt
+++ b/src/nl/hannahsten/texifyidea/reference/InputFileReference.kt
@@ -17,11 +17,8 @@ import nl.hannahsten.texifyidea.psi.LatexCommands
 import nl.hannahsten.texifyidea.psi.LatexPsiHelper
 import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
 import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
-import nl.hannahsten.texifyidea.util.LatexmkRcFileFinder
-import nl.hannahsten.texifyidea.util.expandCommandsOnce
+import nl.hannahsten.texifyidea.util.*
 import nl.hannahsten.texifyidea.util.files.*
-import nl.hannahsten.texifyidea.util.includedPackages
-import nl.hannahsten.texifyidea.util.isTestProject
 import nl.hannahsten.texifyidea.util.magic.CommandMagic

 /**
@@ -122,15 +119,17 @@ class InputFileReference(

         // Check environment variables
         val runManager = RunManagerImpl.getInstanceImpl(element.project) as RunManager
-        val texInputPath = runManager.allConfigurationsList
+        val texinputsVariable = runManager.allConfigurationsList
             .filterIsInstance<LatexRunConfiguration>()
             .firstOrNull { it.mainFile in rootFiles }
             ?.environmentVariables
             ?.envs
             ?.getOrDefault("TEXINPUTS", null)
+            // Not sure which of these takes precedence, or if they are joined together
             ?: LatexmkRcFileFinder.getTexinputsVariable(element.containingFile, null)
+            ?: runCommand("kpsewhich", "--expand-var", "'\\$TEXINPUTS'")

-        if (texInputPath != null) {
+        for (texInputPath in texinputsVariable?.split(":")?.filter { it.isNotBlank() } ?: emptyList()) {
             val path = texInputPath.trimEnd(':')
             searchPaths.add(path.trimEnd('/'))
             // See the kpathsea manual, // expands to subdirs
diff --git a/src/nl/hannahsten/texifyidea/util/LatexmkRcFileFinder.kt b/src/nl/hannahsten/texifyidea/util/LatexmkRcFileFinder.kt
index 4d933f10d..6096a13ff 100644
--- a/src/nl/hannahsten/texifyidea/util/LatexmkRcFileFinder.kt
+++ b/src/nl/hannahsten/texifyidea/util/LatexmkRcFileFinder.kt
@@ -107,7 +107,7 @@ object LatexmkRcFileFinder {
      * Get TEXINPUTS from latexmkrc.
      */
     private fun getTexinputs(file: VirtualFile): String? {
-        return """ensure_path\\('TEXINPUTS',\\s*'(?<path>[^']+)'\\)""".toRegex().find(file.inputStream.reader().readText())?.groups?.get("path")?.value
+        return """ensure_path\\(\\s*'TEXINPUTS',\\s*'(?<path>[^']+)'\\s*\\)""".toRegex().find(file.inputStream.reader().readText())?.groups?.get("path")?.value
     }

     private var usesLatexmkrc: Boolean? = null

Indexing of TEXINPUTS
### Discussed in https://github.com/Hannah-Sten/TeXiFy-IDEA/discussions/3035

Originally posted by **mvscheven** April 14, 2023
We are using a lot of custom classes and style files to keep the preamble of the main tex-file clean. If these custom files are in the source directory, they are known by TeXiFy and included commands are recognized. But if these files are in a location specified by the TEXINPUTS environment variable this does not work. Is it possible to index also files located at TEXINPUTS?

Thanks for any advice!

### Minimal Example:
#### main.tex
```
\\documentclass[11pt]{scrartcl}
\\RequirePackage{my_style}

\\begin{document}
    \\begin{align}
        \\sigma = E \\varepsilon
    \\end{align}
\\end{document}
```
#### my_style.sty
```
\\RequirePackage{amsmath}
```
If my_style.sty is in the source directory, it is linked in the margin next to the RequirePackage command and align is recognized as valid environment.
If my_style.sty is in a TEXINPUTS directory, it is marked as "not found" and for align an error is produced: "Environment requires amsmath package"

### System:
Windows 11, MikTex, IntelliJ IDEA 2023.1, TexiFy IDEA 0.7.28

package nl.hannahsten.texifyidea

import arrow.resilience.Schedule
import arrow.resilience.retry
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import com.intellij.ide.BrowserUtil
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.diagnostic.ErrorReportSubmitter
import com.intellij.openapi.diagnostic.IdeaLoggingEvent
import com.intellij.openapi.diagnostic.SubmittedReportInfo
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.Consumer
import kotlinx.coroutines.runBlocking
import org.apache.maven.artifact.versioning.DefaultArtifactVersion
import java.awt.Component
import java.io.UnsupportedEncodingException
import java.net.*
import kotlin.time.Duration.Companion.milliseconds

/**
 * Send error report to GitHub issue tracker.
 *
 * @author Sten Wessel
 */
class LatexErrorReportSubmitter : ErrorReportSubmitter() {

    override fun getReportActionText() = "Report to TeXiFy-IDEA Issue Tracker"

    override fun submit(
        events: Array<out IdeaLoggingEvent>,
        additionalInfo: String?,
        parentComponent: Component,
        consumer: Consumer<in SubmittedReportInfo>
    ): Boolean {
        val retrySchedule = Schedule.exponential<Throwable>(250.milliseconds)

        val latestVersion = try {
            runBlocking {
                retrySchedule.retry { getLatestVersion() }
            }
        }
        // Don't do the check when there's no internet connection
        catch (e: UnknownHostException) {
            return true
        }

        if (latestVersion.version.toString().isNotBlank() && DefaultArtifactVersion(currentVersion) < latestVersion.version) {
            val currentIdeaVersion = DefaultArtifactVersion(ApplicationInfo.getInstance().build.asStringWithoutProductCode())
            val requiredIdeaVersion = latestVersion.ideaVersion.sinceBuild

            JBPopupFactory.getInstance().createMessage("")
                .showInCenterOf(parentComponent)

            val message = "Please update your current version ($currentVersion) of TeXiFy to the latest version (${latestVersion.version}) before submitting,\n" +
                "to check if the error is already fixed. Go to Settings > Plugins to update.\n" +
                if (currentIdeaVersion < requiredIdeaVersion) "You first need to update your current IDE version ($currentIdeaVersion) to $requiredIdeaVersion or newer.\n" else ""

            val result = MessageDialogBuilder.okCancel("Update TeXiFy", message)
                .yesText("Cancel Submit") // Sort of the wrong way around, but it suggests to cancel this way
                .noText("Submit Anyway")
                .ask(parentComponent)

            // If the user cancels, don't submit
            return !result
        }

        submit(events, additionalInfo, consumer)
        return true
    }

    private fun submit(events: Array<out IdeaLoggingEvent>?, additionalInfo: String?, consumer: Consumer<in SubmittedReportInfo>): Boolean {
        val event = events?.firstOrNull()
        val title = event?.throwableText?.lineSequence()?.first()
            ?: event?.message
            ?: "Crash Report: <Fill in title>"
        val body = event?.throwableText ?: "Please paste the full stacktrace from the IDEA error popup."

        val builder = StringBuilder(ISSUE_URL)
        try {
            builder.append(URLEncoder.encode(title, ENCODING))
            builder.append("&body=")

            val applicationInfo = ApplicationInfo.getInstance().let { "${it.fullApplicationName} (build ${it.build})" }
            builder.append(URLEncoder.encode("### Type of JetBrains IDE (IntelliJ, PyCharm, etc.) and version\n${applicationInfo}\n\n", ENCODING))
            val systemInfo = "${SystemInfo.OS_NAME} ${SystemInfo.OS_VERSION} (${SystemInfo.OS_ARCH})"
            builder.append(URLEncoder.encode("### Operating System \n$systemInfo\n\n", ENCODING))
            builder.append(URLEncoder.encode("### TeXiFy IDEA version\n$currentVersion\n\n", ENCODING))
            builder.append(URLEncoder.encode("### Description\n", ENCODING))
            builder.append(URLEncoder.encode(additionalInfo ?: "\n", ENCODING))
            builder.append(URLEncoder.encode("\n\n### Stacktrace\n```\n${body.take(6000)}\n```", ENCODING))
        }
        catch (e: UnsupportedEncodingException) {
            consumer.consume(
                SubmittedReportInfo(
                    null,
                    null,
                    SubmittedReportInfo.SubmissionStatus.FAILED
                )
            )
            return false
        }

        BrowserUtil.browse(builder.toString())
        consumer.consume(
            SubmittedReportInfo(
                null,
                "GitHub issue",
                SubmittedReportInfo.SubmissionStatus.NEW_ISSUE
            )
        )
        return true
    }

    companion object {

        private const val ISSUE_URL = "https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/new?labels=crash-report&template=crash_report.md&title="

        private const val JETBRAINS_API_URL = "https://plugins.jetbrains.com/plugins/list?pluginId=9473"

        private const val ENCODING = "UTF-8"

        private var latestVersionCached = IdeaPlugin()

        fun getLatestVersion(): IdeaPlugin {
            if (latestVersionCached.version.toString().isNotBlank()) return latestVersionCached

            // Create xml mapper that doesn't fail on unknown properties. This allows us to only define the properties we need.
            val mapper = XmlMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)

            with(URL(JETBRAINS_API_URL).openConnection() as HttpURLConnection) {
                requestMethod = "GET"
                connectTimeout = 1000
                readTimeout = 1000

                val inputString: String = try {
                    inputStream.reader().use { it.readText() }
                }
                catch (e: SocketTimeoutException) {
                    return latestVersionCached
                }
                latestVersionCached = mapper.readValue(inputString, PluginRepo::class.java)
                    .category
                    ?.maxByOrNull { it.version }
                    ?: latestVersionCached

                return latestVersionCached
            }
        }

        val currentVersion by lazy {
            PluginManagerCore.getPlugin(PluginId.getId("nl.rubensten.texifyidea"))?.version
        }
    }

    /**
     * Plugin repo data class which is the root of the xml response from the [Marketplace API](https://plugins.jetbrains.com/docs/marketplace/api-reference.html).
     */
    data class PluginRepo(
        /**
         * Wrapper for multiple idea-plugin objects. Has to be nullable AND have default value to avoid exception.
         */
        @JacksonXmlProperty(localName = "idea-plugin")
        @JacksonXmlElementWrapper(useWrapping = false)
        val category: List<IdeaPlugin>? = emptyList()
    )

    /**
     * Data class specifying all the properties we need from the idea-plugin tag from the xml response.
     * All properties have to have a default value, because the class needs to have an empty constructor.
     */
    data class IdeaPlugin(
        @JacksonXmlProperty(localName = "version")
        val version: DefaultArtifactVersion = DefaultArtifactVersion(""),
        @JacksonXmlProperty(localName = "idea-version")
        val ideaVersion: IdeaVersion = IdeaVersion()
    )

    data class IdeaVersion(
        @JacksonXmlProperty(isAttribute = true, localName = "since-build")
        val sinceBuild: DefaultArtifactVersion = DefaultArtifactVersion("")
    )
}

package nl.hannahsten.texifyidea

import io.ktor.client.statement.*

/**
 * Exception that is thrown by problems within TeXiFy-IDEA.
 * Consider using PluginException if the exception is not caught within TeXiFy, otherwise the real exception message will be hidden.
 *
 * @author Hannah Schellekens
 */
open class TeXception : RuntimeException {

    constructor() : super()
    constructor(message: String) : super(message)
    constructor(message: String, cause: Throwable) : super(message, cause)
    constructor(cause: Throwable) : super(cause)
}

/**
 * Represents a signal that a request to one of the remote libraries failed.
 */
data class RemoteLibraryRequestFailure(val libraryName: String, val response: HttpResponse)

/**
 * A system command/program that failed to run successfully.
 */
data class CommandFailure(val output: String, val exitCode: Int)

package nl.hannahsten.texifyidea

import com.intellij.openapi.util.IconLoader
import com.intellij.util.PlatformIcons
import java.util.*
import javax.swing.Icon

/**
 * @author Hannah Schellekens, Sten Wessel
 */
object TexifyIcons {

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val LATEX_FILE_BIG = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/latex-file-big.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017-2021 Hannah Schellekens
     */
    val LATEX_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/latex-file.svg", TexifyIcons::class.java
    ) or IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/latex-file-alt.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    val LATEX_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/latex-file_smaller.svg", TexifyIcons::class.java
    ) or IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/latex-file-alt_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2019 Hannah Schellekens
     */
    val TIKZ_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/tikz-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val TIKZ_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/tikz-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val PDF_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/pdf-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val PDF_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/pdf-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DVI_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dvi-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val DVI_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dvi-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val BIBLIOGRAPHY_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/bibliography-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val BIBLIOGRAPHY_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/bibliography-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val CLASS_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/class-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val CLASS_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/class-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOCUMENTED_LATEX_SOURCE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/doc-latex-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val DOCUMENTED_LATEX_SOURCE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/doc-latex-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val STYLE_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/style-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val STYLE_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/style-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val TEMP_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/temp.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val SYNCTEX_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/synctex-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val SYNCTEX_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/synctex-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val AUX_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/aux-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val TABLE_OF_CONTENTS_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/toc-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val BBL_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/bbl-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    private val TEXT_FILE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/text-file.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    private val TEXT_FILE_SMALLER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/text-file_smaller.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val BUILD = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/build.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val BUILD_BIB = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/bib-build.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val LATEX_MODULE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/latex-module.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_LATEX = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-tex.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_COMMAND = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-cmd.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_LABEL = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-lbl.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_BIB = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-bib.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_INCLUDE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-incl.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_ENVIRONMENT = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-env.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2018 Hannah Schellekens
     */
    val DOT_CLASS = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-cls.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_NUMBER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-num.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_CHAPTER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-chap.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_PART = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-part.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_SECTION = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-sec.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_SUBSECTION = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-subsec.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_SUBSUBSECTION = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-subsubsec.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_PARAGRAPH = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-par.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val DOT_SUBPARAGRAPH = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/dot-subpar.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FONT_BOLD = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/font-bold.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FONT_ITALICS = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/font-italics.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FONT_UNDERLINE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/font-underline.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FONT_OVERLINE = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/font-overline.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FONT_SMALLCAPS = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/font-smallcaps.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FONT_TYPEWRITER = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/font-mono.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FONT_STRIKETHROUGH = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/font-strike.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val FONT_SLANTED = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/font-slanted.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val SUMATRA = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/sumatra.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val WORD_COUNT = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/word-count.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val TOGGLE_STAR = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/toggle-star.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val STATS = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/stats.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val RIGHT = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/right.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2018 Hannah Schellekens
     */
    val EQUATION_PREVIEW = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/equation-preview.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2019 Hannah Schellekens
     */
    val TIKZ_PREVIEW = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/tikz-preview.svg", TexifyIcons::class.java
    )

    /**
     * Copyright (c) 2021 Hannah Schellekens
     */
    val SYMBOLS = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/symbols.svg", TexifyIcons::class.java
    )

    // From IntelliJ
    val STRING = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/string.svg", TexifyIcons::class.java
    )

    // From IntelliJ (modified)
    val KEY_REQUIRED = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/key-required.svg", TexifyIcons::class.java
    )

    // From IntelliJ (modified)
    val KEY_USER_DEFINED = IconLoader.getIcon(
        "/nl/hannahsten/texifyidea/icons/key-user.svg", TexifyIcons::class.java
    )

    /**
     * For lookup items that have no real category.
     */
    val MISCELLANEOUS_ITEM: Icon = PlatformIcons.PACKAGE_LOCAL_ICON

    /**
     * Get the file icon object that corresponds to the given file extension.
     *
     *
     * This method ignores case.
     *
     * @param extension
     *              The extension of the file to get the icon of without a dot.
     * @param smaller
     *              `true` for a small icon, `false` for a regular sized icon.
     * @return The Icon that corresponds to the given extension.
     * @throws IllegalArgumentException
     * When `extension` is null.
     */
    fun getIconFromExtension(extension: String?, smaller: Boolean = false): Icon {
        return if (extension == null) {
            FILE
        }
        else when (extension.lowercase(Locale.getDefault())) {
            "tex" -> if (smaller) LATEX_FILE_SMALLER else LATEX_FILE
            "bib" -> if (smaller) BIBLIOGRAPHY_FILE_SMALLER else BIBLIOGRAPHY_FILE
            "cls" -> if (smaller) CLASS_FILE_SMALLER else CLASS_FILE
            "dtx" -> if (smaller) DOCUMENTED_LATEX_SOURCE_SMALLER else DOCUMENTED_LATEX_SOURCE
            "sty" -> if (smaller) STYLE_FILE_SMALLER else STYLE_FILE
            "txt" -> if (smaller) TEXT_FILE_SMALLER else TEXT_FILE
            "tikz" -> if (smaller) TIKZ_FILE_SMALLER else TIKZ_FILE
            "log" -> if (smaller) TEXT_FILE_SMALLER else TEXT_FILE
            "pdf" -> if (smaller) PDF_FILE_SMALLER else PDF_FILE
            "synctex.gz" -> if (smaller) SYNCTEX_FILE_SMALLER else SYNCTEX_FILE
            "dvi" -> if (smaller) DVI_FILE_SMALLER else DVI_FILE
            else -> if (smaller) FILE_SMALLER else FILE
        }
    }

    /**
     * Get a variation on the tex icon if applicable.
     */
    private infix fun Icon.or(icon: Icon): Icon {
        val calendar = Calendar.getInstance()
        if (calendar.get(Calendar.DAY_OF_MONTH) != 1) return this
        if (calendar.get(Calendar.MONTH) != 3) return this
        return icon
    }
}

package org.gnome.evince

import org.freedesktop.dbus.interfaces.DBusInterface

/**
 * This interface can be used to call methods on the dbus.
 *
 * @author Thomas Schouten
 */
@Suppress("FunctionName")
interface Daemon : DBusInterface {

    /**
     * Find a certain document.
     * This seems to be unable to handle spaces in a path: the pdf will open but the process owner will not be returned.
     *
     * @param uri Path to a pdf file, prepended with file://
     * @param spawn Whether to spawn Evince or not.
     * @return The name owner of the evince process for the given document URI.
     */
    fun FindDocument(uri: String, spawn: Boolean?): String
}

package nl.hannahsten.texifyidea.action

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.vfs.LocalFileSystem
import nl.hannahsten.texifyidea.util.magic.FileMagic
import java.io.File

/**
 * Action to delete all auxiliary files.
 *
 * @author Abby Berkers
 */
class DeleteAuxFiles : AnAction() {

    override fun actionPerformed(e: AnActionEvent) {
        val project = getEventProject(e) ?: return
        val basePath = project.basePath ?: return
        File(basePath).walk().filter { it.isFile }
            .filter { it.extension in FileMagic.auxiliaryFileTypes }
            .forEach { it.delete() }
        LocalFileSystem.getInstance().refresh(true)
    }
}

package nl.hannahsten.texifyidea.action

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.showOkCancelDialog
import com.intellij.openapi.vfs.InvalidVirtualFileAccessException
import com.intellij.openapi.vfs.LocalFileSystem
import nl.hannahsten.texifyidea.util.Log
import nl.hannahsten.texifyidea.util.getLatexRunConfigurations
import nl.hannahsten.texifyidea.util.magic.FileMagic
import nl.hannahsten.texifyidea.util.runWriteAction
import java.io.File
import java.io.IOException
import java.security.PrivilegedActionException

/**
 * Similar to [DeleteAuxFiles].
 */
class DeleteGeneratedFiles : AnAction() {

    override fun actionPerformed(event: AnActionEvent) {
        try {
            deleteFiles(event)
        }
        catch (e: PrivilegedActionException) {
            Notification("LaTeX", "Could not delete some files", e.message ?: "", NotificationType.ERROR).notify(event.project)
        }
    }

    private fun deleteFiles(e: AnActionEvent) {
        val project = getEventProject(e) ?: return
        val basePath = project.basePath ?: return

        // Custom output folders
        val customOutput = project.getLatexRunConfigurations()
            .flatMap { listOf(it.outputPath.getAndCreatePath(), it.auxilPath.getAndCreatePath()) }
            // There's no reason to delete files outside the project
            .filter { it?.path?.contains(project.basePath!!) == true }
            .filterNotNull()

        val result = showOkCancelDialog(
            "Delete Auxiliary and Output Files",
            "Do you really want to delete all files in LaTeX output directories, " +
                "and all auxiliary and generated files? \n" +
                "All files in the following output directories will be deleted: \n" +
                customOutput.map { it.path }.joinToString { "  $it\n" } +
                "plus auxiliary and generated files in src/, auxil/ and out/.\n" +
                "Be careful when doing this, you might not be able to fully undo this operation!",
            "Delete"
        )

        if (result != Messages.OK) return

        // Also clear aux files
        DeleteAuxFiles().actionPerformed(e)

        // Delete files only in specific folders, to avoid deleting for example figures with pdf extension
        for (folder in setOf("src")) {
            File(basePath, folder).walk().maxDepth(1)
                .filter { it.isFile }
                .filter { it.extension in FileMagic.generatedFileTypes }
                .forEach { it.delete() }
        }

        // Just delete everything in directories which should only contain output files
        val defaultOutput = setOf(File(basePath, "auxil"), File(basePath, "out"))
        for (path in defaultOutput) {
            path.walk().maxDepth(1).forEach { it.delete() }
        }

        val notDeleted = mutableListOf<String>()

        // Custom out/aux dirs
        runWriteAction {
            for (path in customOutput) {
                try {
                    path.children?.forEach {
                        try {
                            it.delete(this)
                        }
                        catch (e: IOException) {
                            Log.warn(e.message ?: e.toString())
                            notDeleted.add(it.path)
                        }
                    }
                }
                catch (e: InvalidVirtualFileAccessException) {
                    Log.warn(e.message ?: e.toString())
                    notDeleted.add(path.path)
                }
            }
        }

        if (notDeleted.isNotEmpty()) {
            Notification("LaTeX", "Could not delete some files", "The following files need to be deleted manually: $notDeleted", NotificationType.WARNING).notify(project)
        }

        // Generated minted files
        File(basePath, "src").walk().maxDepth(1)
            .filter { it.name.startsWith("_minted") }
            .forEach { it.deleteRecursively() }

        LocalFileSystem.getInstance().refresh(true)
    }
}

package nl.hannahsten.texifyidea.action

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.file.LatexFile
import java.util.*
import javax.swing.Icon

/**
 * Action that fetches the required information beforehand.
 *
 * @author Hannah Schellekens
 */
abstract class EditorAction(val name: String, icon: Icon?) : AnAction(name, null, icon) {

    /**
     * Gets called every time the action gets executed.
     *
     * @param file
     * The file that was focussed on when the action was performed.
     * @param project
     * The project that was open when the action was performed.
     * @param textEditor
     * The active editor when the action was performed.
     */
    abstract fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor)

    /**
     * Get the PsiElement that is selected in the editor.
     */
    protected fun getElement(file: VirtualFile, project: Project, textEditor: TextEditor): PsiElement? {
        val psiFile = getPsiFile(file, project) as? LatexFile ?: return null

        val offset = textEditor.editor.caretModel.offset
        return psiFile.findElementAt(offset)
    }

    protected fun getPsiFile(file: VirtualFile, project: Project): PsiFile? {
        return PsiManager.getInstance(project).findFile(file)
    }

    protected fun runWriteAction(project: Project, file: VirtualFile, writeAction: () -> Unit) {
        if (file.isWritable) {
            ApplicationManager.getApplication().runWriteAction {
                CommandProcessor.getInstance().executeCommand(project, writeAction, name, "Texify")
            }
        }
    }

    override fun actionPerformed(event: AnActionEvent) {
        val file = event.getData(PlatformDataKeys.VIRTUAL_FILE)
        val project = event.getData(PlatformDataKeys.PROJECT)
        if (file == null || project == null) {
            return
        }

        val editor = getTextEditor(project, event.getData(PlatformDataKeys.FILE_EDITOR)) ?: return

        actionPerformed(file, project, editor)
    }

    fun getTextEditor(project: Project, fileEditor: FileEditor?): TextEditor? {
        if (fileEditor is TextEditor) {
            return fileEditor
        }

        val editors = FileEditorManager.getInstance(project).selectedEditors
        return Arrays.stream(editors)
            .filter { e -> e is TextEditor }
            .map { e -> e as TextEditor }
            .findFirst()
            .orElse(null)
    }
}

package nl.hannahsten.texifyidea.action

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer
import nl.hannahsten.texifyidea.run.pdfviewer.ExternalPdfViewer
import nl.hannahsten.texifyidea.run.pdfviewer.PdfViewer
import nl.hannahsten.texifyidea.util.selectedRunConfig

open class ForwardSearchAction(var viewer: PdfViewer? = null) : EditorAction(
    name = "_Forward Search",
    icon = TexifyIcons.RIGHT
) {

    override fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor) {
        if (viewer?.isAvailable() != true || file.fileType !is LatexFileType) return

        val document = textEditor.editor.document
        val line = document.getLineNumber(textEditor.editor.caretModel.offset) + 1

        when (viewer) {
            is ExternalPdfViewer -> (viewer as ExternalPdfViewer).forwardSearch(null, file.path, line, project, focusAllowed = true)
            is InternalPdfViewer -> (viewer as InternalPdfViewer).conversation?.forwardSearch(null, file.path, line, project, focusAllowed = true)
            else -> return
        }
    }

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = e.project?.selectedRunConfig()?.pdfViewer == viewer
            && e.getData(CommonDataKeys.VIRTUAL_FILE)?.fileType is LatexFileType
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.action

import com.intellij.openapi.editor.CaretModel
import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.parser.isLatexOrBibtex
import javax.swing.Icon

/**
 * @author Hannah Schellekens
 */
open class InsertEditorAction(
    /**
     * The name of the action.
     */
    name: String,
    /**
     * The icon of the action.
     */
    icon: Icon?,
    /**
     * The text to insert before the selection.
     */
    before: String?,
    /**
     * The text to insert after the selection.
     */
    after: String?
) : EditorAction(name, icon) {

    /**
     * What to insert before the selection.
     */
    private val before: String = before ?: ""

    /**
     * What to insert after the selection.
     */
    private val after: String = after ?: ""

    override fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor) {
        val editor = textEditor.editor
        val document = editor.document
        val selection = editor.selectionModel
        val start = selection.selectionStart
        val end = selection.selectionEnd

        // Don't touch any file content that is not related to TeXiFy
        if (file.psiFile(project)?.findElementAt(start)?.isLatexOrBibtex() != true) return

        runWriteAction(project, file) { insert(document, start, end, editor.caretModel) }
    }

    private fun insert(document: Document, start: Int, end: Int, caretModel: CaretModel) {
        document.insertString(end, this.after)
        document.insertString(start, this.before)

        val caretPosition = if (start == end) {
            start + this.before.length
        }
        else {
            end + this.before.length + this.after.length
        }

        caretModel.moveToOffset(caretPosition)
    }
}

package nl.hannahsten.texifyidea.action

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.source.tree.LeafPsiElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.lang.magic.magicComment
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexGroup
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.parser.getParentOfType
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * @author Hannah Schellekens
 */
class LatexToggleStarAction : EditorAction("Toggle Star", TexifyIcons.TOGGLE_STAR) {

    override fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor) {
        val element = getElement(file, project, textEditor)
        val editor = textEditor.editor
        val psiFile = getPsiFile(file, project)

        element?.parentOfType(LatexGroup::class)?.let {
            println(it.magicComment())
        }

        val commands = getParentOfType(element, LatexCommands::class.java) ?: return

        runWriteAction(project, file) { toggleStar(editor, psiFile, commands) }
    }

    /**
     * Removes the star from a latex commands or adds it when there was no star in the first place.
     *
     * @param editor
     *          The current editor.
     * @param psiFile
     *          The current file.
     * @param commands
     *          The latex command to toggle the star of.
     */
    private fun toggleStar(editor: Editor, psiFile: PsiFile?, commands: LatexCommands?) {
        if (removeStar(commands!!)) {
            return
        }

        addStar(editor, psiFile, commands)
    }

    /**
     * Removes the star from a LaTeX command.
     *
     * @param commands
     *          The command to remove the star from.
     * @return `true` when the star was removed, `false` when the star was not removed.
     */
    private fun removeStar(commands: LatexCommands): Boolean {
        val lastChild = commands.lastChild
        var elt: PsiElement? = commands.firstChild
        while (elt !== lastChild && elt != null) {
            if (elt !is LeafPsiElement) {
                elt = elt.nextSibling
                continue
            }

            if (elt.elementType === LatexTypes.STAR) {
                elt.delete()
                return true
            }
            elt = elt.nextSibling
        }

        return false
    }

    /**
     * Adds a star to a latex command.
     *
     * @param editor
     *          The current editor.
     * @param file
     *          The current file.
     * @param commands
     *          The latex command to add a star to.
     */
    private fun addStar(editor: Editor, file: PsiFile?, commands: LatexCommands) {
        val document = editor.document
        var position = editor.caretModel.offset
        while (position > 0) {
            val text = document.getText(TextRange(position, position + 1))
            val elt = file!!.findElementAt(position)
            val parent = getParentOfType(elt, LatexCommands::class.java)

            if (text.equals("\\", ignoreCase = true) && (elt == null || parent === commands)) {
                document.insertString(position + commands.commandToken.text.length, "*")
                return
            }

            position--
        }
    }
}

package nl.hannahsten.texifyidea.action

import com.intellij.ide.actions.CreateElementActionBase
import com.intellij.ide.actions.CreateFileFromTemplateDialog
import com.intellij.ide.actions.CreateFileFromTemplateDialog.FileCreator
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.file.*
import nl.hannahsten.texifyidea.templates.LatexTemplatesFactory
import nl.hannahsten.texifyidea.templates.LatexTemplatesFactory.Companion.createFromTemplate
import nl.hannahsten.texifyidea.util.appendExtension
import nl.hannahsten.texifyidea.util.files.FileUtil.fileTypeByExtension
import java.util.*
import java.util.function.Consumer

/**
 * @author Hannah Schellekens
 */
class NewLatexFileAction : CreateElementActionBase("LaTeX File", "Create a new LaTeX file", TexifyIcons.LATEX_FILE) {

    override fun invokeDialog(project: Project, psiDirectory: PsiDirectory, elementsConsumer: Consumer<in Array<PsiElement>>) {
        val fileCreator = LatexFileCreator(project, psiDirectory)
        val builder = CreateFileFromTemplateDialog.createDialog(project)
        builder.setTitle("Create a New LaTeX File")
        builder.addKind("Sources (.tex)", TexifyIcons.LATEX_FILE, OPTION_TEX_FILE)
        builder.addKind("Bibliography (.bib)", TexifyIcons.BIBLIOGRAPHY_FILE, OPTION_BIB_FILE)
        builder.addKind("Package (.sty)", TexifyIcons.STYLE_FILE, OPTION_STY_FILE)
        builder.addKind("Document class (.cls)", TexifyIcons.CLASS_FILE, OPTION_CLS_FILE)
        builder.addKind("TikZ (.tikz)", TexifyIcons.TIKZ_FILE, OPTION_TIKZ_FILE)
        builder.show<PsiElement>("", null, fileCreator) {}
    }

    override fun create(s: String, psiDirectory: PsiDirectory): Array<PsiElement> {
        return arrayOf()
    }

    override fun getErrorTitle(): String {
        return "Error"
    }

    override fun getActionName(psiDirectory: PsiDirectory, s: String): String {
        return ""
    }

    private inner class LatexFileCreator(private val project: Project, private val directory: PsiDirectory) : FileCreator<PsiElement?> {

        private fun openFile(virtualFile: VirtualFile) {
            val fileEditorManager = FileEditorManager.getInstance(project)
            fileEditorManager.openFile(virtualFile, true)
        }

        private fun getTemplateNameFromExtension(extensionWithoutDot: String): String {
            return when (extensionWithoutDot) {
                OPTION_STY_FILE -> LatexTemplatesFactory.fileTemplateSty
                OPTION_CLS_FILE -> LatexTemplatesFactory.fileTemplateCls
                OPTION_BIB_FILE -> LatexTemplatesFactory.fileTemplateBib
                OPTION_TIKZ_FILE -> LatexTemplatesFactory.fileTemplateTikz
                else -> LatexTemplatesFactory.fileTemplateTex
            }
        }

        private fun getFileType(fileName: String, option: String): FileType {
            val smallFileName = fileName.lowercase(Locale.getDefault())
            if (smallFileName.endsWith(".$OPTION_TEX_FILE")) {
                return LatexFileType
            }
            if (smallFileName.endsWith(".$OPTION_CLS_FILE")) {
                return ClassFileType
            }
            if (smallFileName.endsWith(".$OPTION_STY_FILE")) {
                return StyleFileType
            }
            if (smallFileName.endsWith(".$OPTION_BIB_FILE")) {
                return BibtexFileType
            }
            return if (smallFileName.endsWith(".$OPTION_TIKZ_FILE")) {
                TikzFileType
            }
            else fileTypeByExtension(option)
        }

        private fun getNewFileName(fileName: String, fileType: FileType): String {
            val smallFileName = fileName.lowercase(Locale.getDefault())
            return if (smallFileName.endsWith("." + fileType.defaultExtension)) {
                smallFileName
            }
            else fileName.appendExtension(fileType.defaultExtension)
        }

        override fun createFile(fileName: String, option: String): PsiElement {
            val fileType = getFileType(fileName, option)
            val newFileName = getNewFileName(fileName, fileType)
            val templateName = getTemplateNameFromExtension(fileType.defaultExtension)
            val file = createFromTemplate(
                directory, newFileName,
                templateName, fileType
            )
            openFile(file.virtualFile)
            return file
        }

        override fun getActionName(fileName: String, option: String): String {
            return "New LaTeX File"
        }

        override fun startInWriteAction(): Boolean {
            return false
        }
    }

    companion object {

        private const val OPTION_TEX_FILE = "tex"
        private const val OPTION_STY_FILE = "sty"
        private const val OPTION_CLS_FILE = "cls"
        private const val OPTION_BIB_FILE = "bib"
        private const val OPTION_TIKZ_FILE = "tikz"
    }
}

package nl.hannahsten.texifyidea.algorithm

/**
 * Why did I make an interface for this?
 *
 *
 * Cuz reasons.
 *
 *
 * Information about the algorithm can only be obtained after executing it with
 * [Algorithm.execute].
 *
 * @author Hannah Schellekens
 */
interface Algorithm {

    /**
     * Executes the algorithm.
     *
     *
     * Information about the algorithm can only be obtained after executing it.
     */
    fun execute()
}

package nl.hannahsten.texifyidea.algorithm

import java.util.*

/**
 * A generic implementation of the breadth first search algorithm.
 * Creates a new BFS that will keep on branching.
 *
 * By default, the algorithm will not fire an action for each visited node (including the
 * starting node). You can alter
 * this by setting an iteration action ([BFS.iterationAction]).
 *
 * If you want to...
 *
 *  * Find the shortest path, use [BFS.shortestPath].
 *  * Execute something at every visited node, use
 * [BFS] in
 * combination with [BFS.iterationAction].
 *
 *
 *
 * @param <N>
 * The node type.
 * @param startNode
 * The node where the BFS should start.
 * @param adjacencyFunction
 * Function that fetches all adjacent nodes of a given node with the given node
 * *excluded*.
 *
 * @author Hannah Schellekens
 */
class BFS<N>(startNode: N, private val adjacencyFunction: (N) -> List<N>, endNode: N? = null) : PathAlgorithm<N> {

    /**
     * The node to start branching from.
     */
    private val start: BFSNode = BFSNode(startNode, 0)

    /**
     * The node where the pathfinding should end, or has ended.
     *
     *
     * Or `null` when the algorithm should branch over all nodes.
     */
    var end: N? = endNode

    /**
     * Set containing all the nodes that have been visited by the algorithm.
     */
    private var visited: MutableSet<N>? = null

    /**
     * A shortest path from [BFS.start] to [BFS.end].
     *
     *
     * `null` if no shortest path is found.
     */
    private var shortestPath: List<N>? = null

    /**
     * Iteration action that literally does nothing and lets the algorithm continue.
     */
    @Suppress("MemberVisibilityCanBePrivate", "PropertyName")
    val NO_ITERATION_ACTION: (N) -> BFSAction = { BFSAction.CONTINUE }

    /**
     * The action to execute for each node or [BFS.NO_ITERATION_ACTION] if you want to
     * execute nothing. The result of the function (a [BFSAction]) determines if the
     * algorithm should continue or not.
     */
    var iterationAction: (N) -> BFSAction = NO_ITERATION_ACTION

    override fun execute() {
        // Terminate when the end is also the start node, but only if the algorithm
        // has a goal (aka end-node) in mind.
        if (!hasGoal()) {
            if (start.node == end) {
                return
            }
        }

        // Queue that contains all the nodes that have to be covered next.
        val toCover: Deque<BFSNode> = ArrayDeque()

        // Initialise state: mark only the start node as visited.
        visited = HashSet()
        (visited as HashSet<N>).add(start.node)
        toCover.add(start)

        // Execute iteration action for the starting node. Abort algorithm if needed.
        val startAction = iterationAction(start.node)
        if (startAction == BFSAction.ABORT) {
            end = start.node
            return
        }

        // Keep on keepin' on until all nodes are covered.
        while (toCover.size > 0) {
            // The node that is currently being processed.
            val current = toCover.remove()

            // Process all adjacent nodes.
            val adjacencyList = getAdjacencyList(current)
            for (adjacentNode in adjacencyList) {
                // Don't process a node that has already been visited.
                if (isVisited(adjacentNode)) {
                    continue
                }

                // Setup distance & predecessor for discovered node.
                val newNode = BFSNode(adjacentNode, current.distance + 1)
                newNode.predecessor = current

                // Execute iteration action for every node visited. Abort algorithm if needed.
                val action = iterationAction(adjacentNode)
                if (action == BFSAction.ABORT) {
                    end = adjacentNode
                    return
                }

                // When looking for a shortest path, terminate when it has been found.
                if (!hasGoal() && newNode.node == end) {
                    shortestPath = constructShortestPath(newNode)
                    return
                }

                // Make sure the adjacent node gets processed during the next iteration.
                toCover.add(newNode)
                markVisited(newNode)
            }
        }
    }

    /**
     * Get the shortest path from the starting node to the ending node.
     *
     *
     * This method requires that the BFS ran in goal-oriented mode. This means that the BFS will
     * actively search for a path.
     *
     *
     * Also, [BFS.execute] must have been called, otherwise the algorithm didn't even
     * have a chance of calculating it.
     *
     * @return The shortest path from the start node to the end node.
     * @throws IllegalStateException
     * When the BFS is not set to find a shortest path, i.e. covers all nodes, or when
     * [BFS.execute] hasn't been called.
     */
    override val path: List<N>
        get() {
            checkNotNull(shortestPath) { "The BFS has no found shortest path." }
            return shortestPath as List<N>
        }

    /**
     * Marks that the given node is visited.
     *
     * @param node
     * The node to mark as visited.
     */
    private fun markVisited(node: BFSNode) {
        visited!!.add(node.node)
    }

    /**
     * Checks if the given node has already been visited by the algorithm.
     *
     * @param node
     * The node to check for if it's been visited already.
     * @return `true` if the node has been visited, `false` when the node hasn't been
     * visited.
     */
    private fun isVisited(node: N): Boolean {
        return visited!!.contains(node)
    }

    /**
     * Get all the adjacent nodes of a given BFSNode.
     *
     * @param node
     * The node to get all adjacent nodes of.
     * @return A list containing all adjacent nodes relative to `node`.
     */
    private fun getAdjacencyList(node: BFSNode): List<N> {
        return adjacencyFunction(node.node)
    }

    /**
     * Reconstructs the shortest path from the end node towards the node with distance 0.
     *
     * @param end
     * The end node from which to backtrack to the starting node.
     * @return The shortest path from start to end nodes.
     */
    private fun constructShortestPath(end: BFSNode): List<N> {
        val shortestPath: MutableList<N> = ArrayList()
        var node = end
        while (node.distance != 0) {
            shortestPath.add(0, node.node)
            node = node.predecessor!!
        }
        return shortestPath
    }

    /**
     * Checks if the BFS algorithm will visit all nodes.
     *
     *
     * In other words: if the BFS will stop at a target node or not.
     *
     * @return `true` if the algorithm stop only when all nodes are covered: the algorithm
     * executes without a goal in mind, `false` if the algorithm stops when the target has
     * been found.
     */
    private fun hasGoal(): Boolean {
        return end == null
    }

    /**
     * Class used by the BFS algorithm to keep track of augmented values.
     *
     * @author Hannah Schellekens, Dylan ter Veen
     */
    private inner class BFSNode(
        /**
         * The original version of the node.
         */
        val node: N,
        /**
         * The distance from the starting node to this node.
         */
        var distance: Int
    ) {

        /**
         * Get the original (wrapped) node.
         *
         * @return The wrapped node object.
         */
        /**
         * Get the distance from the starting node to this node.
         */
        /**
         * Set the distance from the starting node to this node.
         *
         * @param distance
         * The new distance from the start node to this node.
         */
        /**
         * Get the predecessing node from the start node to this one.
         */
        /**
         * The predecessing node from the start node to this one.
         */
        var predecessor: BFSNode? = null

        override fun equals(other: Any?): Boolean {
            if (this === other) {
                return true
            }
            if (other!!.javaClass != javaClass) {
                return false
            }
            val bfsNode = other as? BFS<*>.BFSNode
            return node == bfsNode?.node
        }

        override fun hashCode(): Int {
            return node.hashCode()
        }
    }

    /**
     * Actions that can be executed after an iteration action.
     *
     * @author Hannah Schellekens
     */
    enum class BFSAction {

        CONTINUE, ABORT;
    }
}

package nl.hannahsten.texifyidea.algorithm

import java.util.*

/**
 * Depth first search.
 *
 * @author Hannah Schellekens
 */
open class DFS<Node>(

    /**
     * The starting node.
     */
    val start: Node,

    /**
     * Function that gets all the children of a certain node.
     */
    val children: (Node) -> Collection<Node>,
) {

    /**
     * @return All direct and indirect children of the start node, excluding the start node.
     */
    open fun execute(): Set<Node> {
        // Maintain list of visited nodes, to detect loops
        val visited = mutableSetOf<Node>()

        // Iterative implementation. Yay.
        val stack: Deque<Node> = ArrayDeque()
        stack.push(start)

        while (!stack.isEmpty()) {
            val child = stack.pop()
            visited.add(child)

            // Don't visit nodes twice to avoid loops
            children(child).filter { it !in visited }.forEach(stack::push)
        }

        visited.remove(start)
        return visited
    }
}

package nl.hannahsten.texifyidea.algorithm

/**
 * A path algorithm calculates a path of some sort from a starting node to and end node.
 *
 *
 * Executing the algorithm
 *
 * @param <N>
 * The type the nodes have.
 * @author Hannah Schellekens
</N> */
interface PathAlgorithm<N> : Algorithm {

    /**
     * Get the path that has been found by the algorithm.
     *
     *
     * [Algorithm.execute] must have been executed prior.
     *
     * @return The path that was found by the algorithm.
     * @throws IllegalStateException
     * When the algorithm hasn't been executed first using [Algorithm.execute].
     */
    @get:Throws(IllegalStateException::class)
    val path: List<N>

    /**
     * Get the length of the found path.
     *
     *
     * [Algorithm.execute] must have been executed prior.
     *
     * @return The length of the found path.
     * @throws IllegalStateException
     * When the algorithm hasn't been executed first using [Algorithm.execute].
     */
    @Throws(IllegalArgumentException::class)
    fun size(): Int {
        return path.size
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.patterns.PlatformPatterns
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.completion.pathcompletion.LatexFileProvider
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.magic.FileMagic
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class BibtexCompletionContributor : CompletionContributor() {

    init {
        registerTypeCompletion()
        registerKeyCompletion()
        registerStringCompletion()
        registerFileCompletion()
    }

    /**
     * Adds @type functionality to the autocomplete.
     */
    private fun registerTypeCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement(BibtexTypes.TYPE_TOKEN)
            .andNot(PlatformPatterns.psiElement().inside(BibtexEntry::class.java))
            .withLanguage(BibtexLanguage),
        BibtexTypeTokenProvider
    )

    /**
     * Adds key completion to the autocomplete.
     */
    private fun registerKeyCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns
            .psiElement(BibtexTypes.IDENTIFIER)
            .inside(BibtexEntry::class.java)
            .withPattern { psiElement, _ ->
                val entry = psiElement.parentOfType(BibtexEntry::class)
                val type = entry?.firstChildOfType(BibtexType::class)
                if (type?.text?.lowercase(Locale.getDefault()) == "@string") return@withPattern false

                psiElement.hasParent(BibtexEndtry::class) || psiElement.hasParent(BibtexKey::class)
            }
            .withLanguage(BibtexLanguage),
        BibtexKeyProvider
    )

    /**
     * Adds string support to the autocomplete.
     */
    private fun registerStringCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement(BibtexTypes.IDENTIFIER)
            .inside(BibtexEntry::class.java)
            .inside(BibtexContent::class.java)
            .withLanguage(BibtexLanguage),
        BibtexStringProvider
    )

    private fun registerFileCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(BibtexContent::class.java)
            .withPattern("File completion pattern") { psiElement, _ ->
                val key = psiElement.firstParentOfType(BibtexTag::class)?.firstChildOfType(BibtexKey::class) ?: return@withPattern false
                // Currently, the bibsource field is used for file sources, but this is apparently not 'official'
                key.text in FileMagic.bibtexFileKeys
            }
            .withLanguage(BibtexLanguage),
        LatexFileProvider()
    )
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.psi.PsiFile
import com.intellij.util.PlatformIcons
import com.intellij.util.ProcessingContext
import com.intellij.util.containers.ContainerUtil
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.completion.handlers.TokenTypeInsertHandler
import nl.hannahsten.texifyidea.lang.*
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexKey
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * @author Hannah Schellekens
 */
object BibtexKeyProvider : CompletionProvider<CompletionParameters>() {

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val psiElement = parameters.position
        val entry = psiElement.parentOfType(BibtexEntry::class) ?: return
        val token = entry.tokenType()
        val entryType = BibtexDefaultEntryType[token] ?: return
        val optional: Set<BibtexEntryField> = entryType.optional.toSet()
        val required: Set<BibtexEntryField> = entryType.required.toSet()

        // Removed already present items.
        val allFields: Set<BibtexEntryField> = entryType.allFields().toSet()
        val userDefined: Set<BibtexEntryField> = findUserDefinedKeys(entry.containingFile, allFields)
        val keys = entry.keyNames()
        val fields = (allFields + userDefined).toMutableSet()
        fields.removeIf { it.fieldName in keys }

        // Add lookup elements.
        result.addAllElements(
            ContainerUtil.map2List(fields) {
                val (message, icon) = when (it) {
                    in required -> " required" and TexifyIcons.KEY_REQUIRED
                    in optional -> " optional" and PlatformIcons.PROTECTED_ICON
                    in userDefined -> " custom" and TexifyIcons.KEY_USER_DEFINED
                    else -> "" and PlatformIcons.PROTECTED_ICON
                }

                LookupElementBuilder.create(it, it.fieldName)
                    .withPresentableText(it.fieldName)
                    .bold()
                    .withTypeText(message, true)
                    .withIcon(icon)
                    .withTailText(packageName(it), true)
                    .withInsertHandler(TokenTypeInsertHandler)
            }
        )
    }

    /**
     * Scans the given file for fields that are defined by the user.
     *
     * @param file
     *          The file to scan.
     * @param allFields
     *          All fields that are already in the autocomplete.
     * @return A list containing all user defined keys.
     */
    private fun findUserDefinedKeys(file: PsiFile, allFields: Collection<BibtexEntryField>): Set<BibtexEntryField> {
        val result = HashSet<BibtexEntryField>()
        val presentFieldSet: MutableSet<String> = allFields.map { it.fieldName }
            .toMutableSet()

        for (key in file.childrenOfType(BibtexKey::class)) {
            val name = key.text
            if (name !in presentFieldSet) {
                presentFieldSet.add(name)
                result.add(SimpleBibtexEntryField(name, "User defined string."))
            }
        }

        return result
    }

    private fun packageName(dependend: Dependend): String {
        return when (val dependency = dependend.dependency) {
            LatexPackage.DEFAULT -> ""
            else -> "  (${dependency.name})"
        }
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.ProcessingContext
import com.intellij.util.containers.ContainerUtil
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.lang.Described
import nl.hannahsten.texifyidea.psi.BibtexComment
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexTag
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.lastChildOfType
import nl.hannahsten.texifyidea.util.parser.previousSiblingIgnoreWhitespace

/**
 * I think this provides autocompletion for strings defined with @string{..} commands.
 *
 * @author Hannah Schellekens
 */
object BibtexStringProvider : CompletionProvider<CompletionParameters>() {

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val psiFile = parameters.originalFile
        val strings: List<Triple<String, String, BibtexEntry>> = psiFile.childrenOfType(BibtexEntry::class).asSequence()
            .filter { it.tokenType() == "@string" }
            .mapNotNull {
                val tag = it.firstChildOfType(BibtexTag::class) ?: return@mapNotNull null
                val key = tag.key
                val content = tag.content ?: return@mapNotNull null
                Triple(key.text, content.text, it)
            }
            .toList()

        result.addAllElements(
            ContainerUtil.map2List(strings) {
                LookupElementBuilder.create(StringDescription(it!!.third), it.first)
                    .withPresentableText(it.first)
                    .bold()
                    .withTypeText(it.second, true)
                    .withIcon(TexifyIcons.STRING)
            }
        )
    }

    class StringDescription(entry: BibtexEntry?) : Described {

        override val description: String

        init {
            val previous = entry?.previousSiblingIgnoreWhitespace()
            val comment = previous?.lastChildOfType(BibtexComment::class) ?: entry?.previousSiblingIgnoreWhitespace()
            this.description = comment?.text?.replace(Regex("^%\\s?"), "") ?: "User defined string."
        }
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.PlatformIcons
import com.intellij.util.ProcessingContext
import nl.hannahsten.texifyidea.completion.handlers.TokenTypeInsertHandler
import nl.hannahsten.texifyidea.lang.BibtexDefaultEntryType
import nl.hannahsten.texifyidea.lang.LatexPackage
import java.util.*

/**
 * @author Hannah Schellekens
 */
object BibtexTypeTokenProvider : CompletionProvider<CompletionParameters>() {

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        result.addAllElements(
            BibtexDefaultEntryType.values().map {
                LookupElementBuilder.create(it, it.token)
                    .withPresentableText(it.token)
                    .bold()
                    .withIcon(PlatformIcons.ANNOTATION_TYPE_ICON)
                    .withTailText(" " + tags(it) + " " + packageName(it), true)
                    .withInsertHandler(TokenTypeInsertHandler)
            }
        )
    }

    private fun tags(entry: BibtexDefaultEntryType): String {
        return " {" + entry.required.joinToString { it.toString().lowercase(Locale.getDefault()) } + "}"
    }

    private fun packageName(entry: BibtexDefaultEntryType): String {
        return when (val dependency = entry.dependency) {
            LatexPackage.DEFAULT -> ""
            else -> " (${dependency.name})"
        }
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate.Result
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TextExpression
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.source.tree.LeafPsiElement
import nl.hannahsten.texifyidea.file.BibtexFileType
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.nextSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.previousSiblingIgnoreWhitespace
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class InsertBibtexTag : EnterHandlerDelegate {

    override fun postProcessEnter(file: PsiFile, editor: Editor, context: DataContext): Result {
        if (file.fileType != BibtexFileType) {
            return Result.Continue
        }

        val caret = editor.caretModel
        val element = file.findElementAt(caret.offset)
        if (hasValidContext(element, editor)) {
            startTemplate(file, editor)
        }

        return Result.Continue
    }

    override fun preprocessEnter(file: PsiFile, editor: Editor, p2: Ref<Int>, p3: Ref<Int>, context: DataContext, p5: EditorActionHandler?): Result {
        return Result.Continue
    }

    /**
     * Starts the tag insertion template process.
     */
    private fun startTemplate(file: PsiFile, editor: Editor) {
        val template = TemplateImpl("", "\$__Variable0\$ = {\$__Variable1\$},", "")
        template.addVariable(TextExpression("key"), true)
        template.addVariable(TextExpression("value"), true)

        val templateManager = TemplateManager.getInstance(file.project)
        templateManager.startTemplate(editor, template)
    }

    /**
     * Checks whether the element is in such a position where the tag template must be invoked.
     *
     * @return `true` when in valid context, `false` when nothing should happen or when the element is `null`.
     */
    private fun hasValidContext(element: PsiElement?, editor: Editor): Boolean {
        if (element == null) {
            return false
        }

        // Ignore @string and @preamble
        val parent = element.parentOfType(BibtexEntry::class)
        if (parent != null) {
            val token = parent.tokenName().lowercase(Locale.getDefault())
            if (token == "string" || token == "preamble") {
                return false
            }
        }

        // Check context.
        val next = element.nextSiblingIgnoreWhitespace()
        if (next is BibtexTag || next is BibtexEndtry) {
            return true
        }

        val previous = element.previousSiblingIgnoreWhitespace() ?: return false
        if (previous is BibtexId) {
            return true
        }

        // Check when no prior tags existed in the entry.
        val noTags = (
            previous is LeafPsiElement && next is LeafPsiElement &&
                previous.elementType == BibtexTypes.SEPARATOR && next.elementType == BibtexTypes.CLOSE_BRACE
            )
        if (!noTags) {
            return false
        }

        // Insert indentation if needed.
        val caret = editor.caretModel
        val file = element.containingFile
        val document = file.document() ?: return true
        val lineNumber = document.getLineNumber(caret.offset)
        val indent = document.lineIndentation(lineNumber)
        if (indent.length < 4) {
            document.insertString(caret.offset, " ".repeat(4 - indent.length))
        }

        return true
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.impl.CamelHumpMatcher
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.ProcessingContext
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.completion.handlers.LatexReferenceInsertHandler
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.remotelibraries.RemoteLibraryManager
import nl.hannahsten.texifyidea.util.labels.findBibtexItems
import nl.hannahsten.texifyidea.util.parser.getAuthors
import nl.hannahsten.texifyidea.util.parser.getIdentifier
import nl.hannahsten.texifyidea.util.parser.getTitle
import java.util.*

object LatexBibliographyReferenceProvider : CompletionProvider<CompletionParameters>() {

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val localEntries = parameters.originalFile.findBibtexItems()
        // Add the remote entries to the autocompletion, only adding the entries that do not exist in the local bibliography yet.
        val remoteEntries = RemoteLibraryManager.getInstance().getLibraries()
            .flatMap { it.value.entries }
            .toSet()
            // Filter ids that are already included in the local bib entries.
            .filter { it.id !in localEntries.filterIsInstance<BibtexEntry>().map { bib -> bib.id } }

        val lookupItems = localEntries.mapNotNull { bibtexEntry ->
            when (bibtexEntry) {
                is BibtexEntry -> createLookupElementFromBibtexEntry(bibtexEntry)
                is LatexCommands -> createLookupElementFromLatexCommand(bibtexEntry)
                else -> null
            }
        }

        val lookupItemsFromRemote = remoteEntries.map { createLookupElementFromBibtexEntry(it, true) }

        val before = result.prefixMatcher.prefix
        val prefix =
            if (before.contains(',')) {
                before.substring(before.lastIndexOf(',') + 1)
            }
            else {
                before
            }
        result.withPrefixMatcher(CamelHumpMatcher(prefix, false)).addAllElements(lookupItems + lookupItemsFromRemote)
    }

    private fun createLookupElementFromBibtexEntry(bibtexEntry: BibtexEntry, remote: Boolean = false): LookupElementBuilder {
        val lookupStrings = LinkedList(bibtexEntry.getAuthors())
        lookupStrings.add(bibtexEntry.getTitle())
        return LookupElementBuilder.create(bibtexEntry.getIdentifier())
            .withPsiElement(bibtexEntry.id)
            .withPresentableText(bibtexEntry.getTitle())
            .bold()
            .withInsertHandler(LatexReferenceInsertHandler(remote, if (remote) bibtexEntry else null))
            .withLookupStrings(lookupStrings)
            .withTypeText(
                bibtexEntry.getIdentifier(),
                true
            )
            .withIcon(TexifyIcons.DOT_BIB)
    }

    private fun createLookupElementFromLatexCommand(latexCommand: LatexCommands): LookupElementBuilder? {
        if (latexCommand.getRequiredParameters().isEmpty()) return null
        return LookupElementBuilder.create(latexCommand.getRequiredParameters()[0])
            .bold()
            .withInsertHandler(LatexReferenceInsertHandler())
            .withTypeText(latexCommand.containingFile.name + ": " + (1 + StringUtil.offsetToLineNumber(latexCommand.containingFile.text, latexCommand.textOffset)), true)
            .withIcon(TexifyIcons.DOT_BIB)
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.ProcessingContext
import com.intellij.util.containers.ContainerUtil
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens
 */
object LatexBibliographyStyleProvider : CompletionProvider<CompletionParameters>() {

    /**
     * List of all available default bibliography styles.
     */
    private val DEFAULT_STYLES = setOf("abbrv", "acm", "alpha", "apalike", "ieeetr", "plain", "siam", "unsrt")

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        result.addAllElements(
            ContainerUtil.map2List(DEFAULT_STYLES) { name ->
                LookupElementBuilder.create(name, name)
                    .withPresentableText(name)
                    .bold()
                    .withIcon(TexifyIcons.MISCELLANEOUS_ITEM)
            }
        )
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.lookup.CharFilter
import com.intellij.codeInsight.lookup.Lookup
import nl.hannahsten.texifyidea.file.LatexFile

/**
 * @author Sten Wessel
 */
class LatexCharFilter : CharFilter() {

    override fun acceptChar(c: Char, prefixLength: Int, lookup: Lookup): Result? {
        return if (!isInLatexContext(lookup)) {
            null
        }
        else when (c) {
            '$' -> Result.HIDE_LOOKUP
            ':' -> Result.ADD_TO_PREFIX
            else -> null
        }
    }

    companion object {

        private fun isInLatexContext(lookup: Lookup): Boolean {
            if (!lookup.isCompletion) {
                return false
            }
            val element = lookup.psiElement
            val file = lookup.psiFile
            return file is LatexFile && element != null
        }
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.openapi.util.TextRange
import com.intellij.psi.ElementManipulator
import nl.hannahsten.texifyidea.psi.LatexCommands

class LatexCommandElementManipulators : ElementManipulator<LatexCommands> {

    /**
     * Return the text range of the command token, without the text range of the
     * required parameters. When this range also includes the range of the required
     * arguments, the PsiReference for custom commands gets in the way of the
     * completion inside of the required arguments.
     */
    override fun getRangeInElement(element: LatexCommands): TextRange {
        return element.commandToken.textRangeInParent
    }

    override fun handleContentChange(element: LatexCommands, range: TextRange, newContent: String?): LatexCommands? {
        return null
    }

    override fun handleContentChange(element: LatexCommands, newContent: String?): LatexCommands? {
        return null
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.ProcessingContext
import com.intellij.util.indexing.FileBasedIndex
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.completion.LatexEnvironmentProvider.addEnvironments
import nl.hannahsten.texifyidea.completion.LatexEnvironmentProvider.addIndexedEnvironments
import nl.hannahsten.texifyidea.completion.LatexEnvironmentProvider.packageName
import nl.hannahsten.texifyidea.completion.handlers.LatexCommandArgumentInsertHandler
import nl.hannahsten.texifyidea.completion.handlers.LatexMathInsertHandler
import nl.hannahsten.texifyidea.completion.handlers.LatexNoMathInsertHandler
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.index.file.LatexExternalCommandIndex
import nl.hannahsten.texifyidea.lang.LatexMode
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.*
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.settings.sdk.TexliveSdk
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.Kindness.getKindWords
import nl.hannahsten.texifyidea.util.files.*
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.PackageMagic
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * Provide autocompletion for commands and environments.
 *
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexCommandsAndEnvironmentsCompletionProvider internal constructor(private val mode: LatexMode) :
    CompletionProvider<CompletionParameters>() {

    companion object {

        /** Cache for commands which are indexed and which should be added to the autocompletion. */
        val indexedCommands = mutableSetOf<LookupElementBuilder>()

        /** 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.isAvailable || ProjectJdkTable.getInstance().allJdks.any { it.sdkType is TexliveSdk }
    }

    override fun addCompletions(
        parameters: CompletionParameters,
        context: ProcessingContext,
        result: CompletionResultSet
    ) {
        when (mode) {
            LatexMode.NORMAL -> {
                addIndexedCommands(result, parameters)
                addNormalCommands(result, parameters.editor.project ?: return)
                addCustomCommands(parameters, result)
            }
            LatexMode.MATH -> {
                addMathCommands(result)
                addCustomCommands(parameters, result, LatexMode.MATH)
            }
            LatexMode.ENVIRONMENT_NAME -> {
                addEnvironments(result, parameters)
                addIndexedEnvironments(result, parameters)
            }
        }
        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.
     */
    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.getItems(project), project).plus(LatexPackage.DEFAULT)

        val lookupElementBuilders = mutableSetOf<LookupElementBuilder>()

        val commandsFromIndex = mutableListOf<Set<LatexCommand>>()

        FileBasedIndex.getInstance().processAllKeys(
            LatexExternalCommandIndex.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) }
                }
        }
        indexedCommands.addAll(lookupElementBuilders)
        result.addAllElements(lookupElementBuilders)
    }

    private fun addNormalCommands(result: CompletionResultSet, project: Project) {
        val indexedKeys = FileBasedIndex.getInstance().getAllKeys(LatexExternalCommandIndex.id, project)

        result.addAllElements(
            LatexRegularCommand.values().flatMap { cmd ->
                /** True if there is a package for which we already have the [cmd] command indexed.  */
                fun alreadyIndexed() =
                    FileBasedIndex.getInstance().getContainingFiles(LatexExternalCommandIndex.id, cmd.commandWithSlash, GlobalSearchScope.everythingScope(project))
                        .map { LatexPackage.create(it) }.contains(cmd.dependency)

                // 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()) {
                    emptyList()
                }
                else {
                    createCommandLookupElements(cmd)
                }
            }
        )
        result.addLookupAdvertisement(getKindWords())
    }

    private fun addMathCommands(result: CompletionResultSet) {
        // Find all commands.
        val commands: MutableList<LatexCommand> = ArrayList(LatexMathCommand.values())
        commands.add(LatexGenericRegularCommand.BEGIN)

        // Create autocomplete elements.
        result.addAllElements(
            commands.flatMap { cmd: LatexCommand ->
                cmd.arguments.toSet().optionalPowerSet().mapIndexed { index, args ->
                    val handler = if (cmd.isMathMode.not()) LatexNoMathInsertHandler(args.toList())
                    else LatexMathInsertHandler(args.toList())
                    LookupElementBuilder.create(cmd, cmd.command + List(index) { " " }.joinToString(""))
                        .withPresentableText(cmd.commandWithSlash)
                        .bold()
                        .withTailText(args.joinToString("") + " " + packageName(cmd), true)
                        .withTypeText(cmd.display)
                        .withInsertHandler(handler)
                        .withIcon(TexifyIcons.DOT_COMMAND)
                }
            }
        )
    }

    private fun addCustomCommands(
        parameters: CompletionParameters, result: CompletionResultSet,
        mode: LatexMode? = null
    ) {
        val file = parameters.originalFile
        val files: MutableSet<PsiFile> = HashSet(file.referencedFileSet())
        val root = file.findRootFile()
        val documentClass = root.documentClassFileInProject()
        if (documentClass != null) {
            files.add(documentClass)
        }
        val cmds = getCommandsInFiles(files, file)
        for (cmd in cmds) {
            if (!cmd.isDefinition() && !cmd.isEnvironmentDefinition()) {
                continue
            }
            if (mode !== LatexMode.MATH && cmd.name in CommandMagic.mathCommandDefinitions) {
                continue
            }
            val cmdName = cmd.definitionCommand()?.name ?: continue

            // Skip over 'private' commands containing @ symbol in normal tex source files.
            if (!file.isClassFile() && !file.isStyleFile()) {
                if (cmdName.contains("@")) {
                    continue
                }
            }
            val arguments = getArgumentsFromDefinition(cmd)
            var typeText = getTypeText(cmd)
            val line = 1 + StringUtil.offsetToLineNumber(cmd.containingFile.text, cmd.textOffset)
            typeText = typeText + " " + cmd.containingFile.name + ":" + line
            result.addAllElements(
                arguments.toSet().optionalPowerSet().mapIndexed { index, args ->
                    LookupElementBuilder.create(cmd, cmdName.substring(1) + List(index) { " " }.joinToString(""))
                        .withPresentableText(cmdName)
                        .bold()
                        .withTailText(args.joinToString(""), true)
                        .withTypeText(typeText, true)
                        .withInsertHandler(LatexCommandArgumentInsertHandler(args.toList()))
                        .withIcon(TexifyIcons.DOT_COMMAND)
                }
            )
        }
        result.addLookupAdvertisement(getKindWords())
    }

    private fun getArgumentsFromDefinition(commands: LatexCommands): List<Argument> {
        val argumentString = getTailText(commands)
        val argumentStrings =
            """(\[\w*])|(\{\w*})""".toRegex().findAll(argumentString).map { it.value }.toList()
        return argumentStrings.map {
            if (it.startsWith("{")) RequiredArgument(it.drop(1).dropLast(1))
            else OptionalArgument(it.drop(1).dropLast(1))
        }
    }

    private fun getTypeText(commands: LatexCommands): String {
        if (commands.commandToken.text in CommandMagic.commandDefinitionsAndRedefinitions) {
            return ""
        }
        val firstNext = commands.nextCommand() ?: return ""
        val secondNext = firstNext.nextCommand() ?: return ""
        val lookup = secondNext.commandToken.text
        return lookup ?: ""
    }

    private fun getTailText(commands: LatexCommands): String {
        return when (commands.commandToken.text) {
            "\\newcommand" -> {
                val optional: List<String> = LinkedList(commands.getOptionalParameterMap().toStringMap().keys)
                var requiredParameterCount = 0
                if (optional.isNotEmpty()) {
                    try {
                        when (optional.size) {
                            1 -> requiredParameterCount = optional[0].toInt()
                            2 -> requiredParameterCount = optional[0].toInt() - 1
                        }
                    }
                    catch (ignore: NumberFormatException) {
                    }
                }

                (if (optional.size == 2) "[args]" else "") +
                    if (requiredParameterCount == 1) "{param}"
                    // Number the required parameters so a toSet call won't merge them. This way the
                    // user can keep them apart as well.
                    else (1..requiredParameterCount).joinToString("") { "{param$it}" }
            }

            "\\DeclarePairedDelimiter" -> "{param}"
            "\\DeclarePairedDelimiterX", "\\DeclarePairedDelimiterXPP" -> {
                val optional = commands.getOptionalParameterMap().toStringMap().keys.firstOrNull()
                val nrParams = try {
                    optional?.toInt() ?: 0
                }
                catch (ignore: java.lang.NumberFormatException) {
                    0
                }
                (1..nrParams).joinToString("") { "{param}" }
            }

            "\\NewDocumentCommand", "\\DeclareDocumentCommand" -> {
                val paramSpecification = commands.getRequiredParameters().getOrNull(1)?.removeAll("null", " ") ?: ""
                paramSpecification.map { c ->
                    if (PackageMagic.xparseParamSpecifiers[c] ?: return@map "") "{param}"
                    else "[]"
                }.joinToString("")
            }

            else -> ""
        }
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.icons.AllIcons
import com.intellij.patterns.PlatformPatterns
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.completion.pathcompletion.LatexFileProvider
import nl.hannahsten.texifyidea.completion.pathcompletion.LatexFolderProvider
import nl.hannahsten.texifyidea.completion.pathcompletion.LatexGraphicsPathProvider
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.inspections.ALL_TEXIFY_INSPECTIONS
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.lang.LatexMode
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.lang.commands.*
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.run.compiler.BibliographyCompiler
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.magic.ColorMagic
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.CommentMagic
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * This class registers some completion contributors. For labels we currently use reference completion instead of
 * contributor-based completion, in [nl.hannahsten.texifyidea.reference.LatexLabelReference],
 * though at the moment I don't see a reason why this is the case.
 * Also see https://www.jetbrains.org/intellij/sdk/docs/reference_guide/custom_language_support/code_completion.html
 *
 * @author Sten Wessel, Hannah Schellekens
 */
open class LatexCompletionContributor : CompletionContributor() {

    init {
        registerRegularCommandCompletion()
        registerMathModeCompletion()
        registerFileNameCompletion()
        registerFolderNameCompletion()
        registerGraphicPathCompletion()
        registerColorCompletion()
        registerMagicCommentCompletion()
        registerBibliographyReferenceCompletion()
        registerPackageNameCompletion()
        registerGlossariesCompletion()
        registerDocumentClassCompletion()
        registerLatexArgumentTypeCompletion()
        registerDefaultEnvironmentCompletion()
    }

    /**
     * Adds the commands outside math context to the autocomplete.
     */
    private fun registerRegularCommandCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement(LatexTypes.COMMAND_TOKEN)
            .andNot(PlatformPatterns.psiElement().inside(LatexMathEnvironment::class.java))
            .withPattern { psiElement, _ -> psiElement.inMathContext().not() }
            .withLanguage(LatexLanguage),
        LatexCommandsAndEnvironmentsCompletionProvider(LatexMode.NORMAL)
    )

    /**
     * Adds the commands inside math context to the autocomplete.
     */
    private fun registerMathModeCompletion() {
        extend(
            CompletionType.BASIC,
            PlatformPatterns.psiElement(LatexTypes.COMMAND_TOKEN)
                .withPattern { psiElement, _ -> psiElement.inMathContext() }
                .withLanguage(LatexLanguage),
            LatexCommandsAndEnvironmentsCompletionProvider(LatexMode.MATH)
        )

        registerMathModeInsideEnvironmentCompletion()
    }

    private fun registerMathModeInsideEnvironmentCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement(LatexTypes.COMMAND_TOKEN)
            .inside(LatexMathEnvironment::class.java)
            .withLanguage(LatexLanguage),
        LatexCommandsAndEnvironmentsCompletionProvider(LatexMode.MATH)
    )

    /**
     * Adds file name support to the autocomplete.
     */
    private fun registerFileNameCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(LatexParameterText::class.java)
            .inside(LatexRequiredParam::class.java)
            .withPattern("File name completion pattern") { psiElement, processingContext ->
                val command = getParentOfType(psiElement, LatexCommands::class.java) ?: return@withPattern false
                val name = command.commandToken.text
                val cmd = LatexRegularCommand[name.substring(1)] ?: return@withPattern false

                val args = cmd.first().getArgumentsOf(RequiredFileArgument::class)
                if (args.isNotEmpty()) processingContext?.put("type", args.first())
                args.isNotEmpty()
            }
            .withLanguage(LatexLanguage),
        LatexFileProvider()
    )

    /**
     * Adds folder name support to the autocomplete.
     */
    private fun registerFolderNameCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(LatexRequiredParam::class.java)
            .withPattern("Folder name completion pattern") { psiElement, processingContext ->
                val command = getParentOfType(psiElement, LatexCommands::class.java) ?: return@withPattern false
                val name = command.commandToken.text
                val cmd = LatexRegularCommand[name.substring(1)] ?: return@withPattern false

                val args = cmd.first().getArgumentsOf(RequiredFolderArgument::class)
                if (args.isNotEmpty()) processingContext?.put("type", args.first())
                args.isNotEmpty()
            }
            .withLanguage(LatexLanguage),
        LatexFolderProvider()
    )

    /**
     * Adds graphics path support to the autocomplete.
     */
    private fun registerGraphicPathCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(LatexParameterText::class.java)
            .inside(LatexRequiredParam::class.java)
            .withPattern("Folder name completion pattern") { psiElement, processingContext ->
                val command = getParentOfType(psiElement, LatexCommands::class.java) ?: return@withPattern false
                val name = command.commandToken.text
                val cmd = LatexRegularCommand[name.substring(1)] ?: return@withPattern false

                val args = cmd.first().getArgumentsOf(RequiredPicturePathArgument::class)
                if (args.isNotEmpty()) processingContext?.put("type", args.first())
                args.isNotEmpty()
            }
            .withLanguage(LatexLanguage),
        LatexGraphicsPathProvider()
    )

    /**
     * Adds color support to the autocomplete.
     */
    private fun registerColorCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(LatexRequiredParam::class.java)
            .withPattern("xcolor color completion pattern") { psiElement, _ ->
                val command = getParentOfType(psiElement, LatexCommands::class.java) ?: return@withPattern false
                val name = command.commandToken.text
                name.substring(1) in ColorMagic.takeColorCommands
            },
        LatexXColorProvider
    )

    /**
     * Adds autocomplete functionality for magic comments.
     */
    private fun registerMagicCommentCompletion() {
        registerMagicCommentKeyCompletion()
        registerMagicCommentValueCompletion()
    }

    /**
     * Adds all available magic comment keys to the autocomple.
     */
    private fun registerMagicCommentKeyCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(LatexMagicComment::class.java)
            .withPattern("Magic comment completion pattern") { comment, _ ->
                comment.containsMagicComment() && comment.text.contains('=').not()
            }
            .withLanguage(LatexLanguage),
        LatexMagicCommentKeyProvider
    )

    /**
     * Adds all possible magic comment values to the autocomplete.
     */
    private fun registerMagicCommentValueCompletion() {
        registerMagicCommentInspectionCompletion()
        registerMagicCommentTikzMathCompletion()
        registerMagicCommentLatexCompilerCompletion()
        registerMagicCommentBibtexCompilerCompletion()
        registerMagicCommentFakeCompletion()
    }

    /**
     * Adds the inspection list to the autocomplete for magic comment suppress values.
     */
    private fun registerMagicCommentInspectionCompletion() {
        val suppressRegex = Regex("""suppress\s*=\s*""", EnumSet.of(RegexOption.IGNORE_CASE))
        val inspectionIds = InsightGroup.byFileType(LatexFileType)
            .flatMap { ALL_TEXIFY_INSPECTIONS[it] ?: emptyList() }
            .toHashSet()
        extendMagicCommentValues(
            "suppress",
            suppressRegex,
            LatexMagicCommentValueProvider(suppressRegex, inspectionIds, AllIcons.General.InspectionsEye)
        )
    }

    /**
     * Adds the list containing tikz/math to the autocomplete for begin/end/preamble values in magic comments.
     */
    private fun registerMagicCommentTikzMathCompletion() {
        val beginEndRegex = Regex("""(begin|end|preview) preamble\s*=\s*""", EnumSet.of(RegexOption.IGNORE_CASE))
        extendMagicCommentValues(
            "preamble",
            beginEndRegex,
            LatexMagicCommentValueProvider(beginEndRegex, CommentMagic.preambleValues)
        )
    }

    /**
     * Adds the list of LaTeX compilers to the autocomplete for magic comment values.
     */
    private fun registerMagicCommentLatexCompilerCompletion() {
        val compilerRegex = Regex("""compiler\s*=\s*""", EnumSet.of(RegexOption.IGNORE_CASE))
        extendMagicCommentValues(
            "compiler",
            compilerRegex,
            LatexMagicCommentValueProvider(compilerRegex, LatexCompiler.values().map { it.executableName }.toHashSet())
        )
    }

    /**
     * Adds the list of Bibtex compilers to the autocomplete for magic comment values.
     */
    private fun registerMagicCommentBibtexCompilerCompletion() {
        val bibtexCompilerRegex = Regex("""bibtex compiler\s*=\s*""", EnumSet.of(RegexOption.IGNORE_CASE))
        extendMagicCommentValues(
            "bibtex compiler",
            bibtexCompilerRegex,
            LatexMagicCommentValueProvider(
                bibtexCompilerRegex,
                BibliographyCompiler.values().map { it.executableName }.toHashSet()
            )
        )
    }

    /**
     * Adds the fake magic comment values to the autocomplete.
     */
    private fun registerMagicCommentFakeCompletion() {
        val fakeRegex = Regex("""fake\s*(=\s*)?""", EnumSet.of(RegexOption.IGNORE_CASE))
        extendMagicCommentValues(
            "fake",
            fakeRegex,
            LatexMagicCommentValueProvider(fakeRegex, CommentMagic.fakeSectionValues)
        )
    }

    /**
     * Adds a completions contributor that gets activated when typing a value for a magic comment.
     */
    private fun extendMagicCommentValues(
        commentName: String,
        regex: Regex,
        completionProvider: CompletionProvider<CompletionParameters>
    ) = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(LatexMagicComment::class.java)
            .withPattern("Magic comment $commentName pattern") { comment, _ ->
                comment.containsMagicComment() && comment.text.contains(regex)
            }
            .withLanguage(LatexLanguage),
        completionProvider
    )

    /**
     * Adds support for bibliography references to the autocomplete.
     */
    private fun registerBibliographyReferenceCompletion() {
        extendLatexCommands(LatexBibliographyReferenceProvider, CommandMagic.bibliographyReference)
    }

    /**
     * Adds support for package names to the autocomplete.
     */
    private fun registerPackageNameCompletion() {
        extendLatexCommands(LatexPackageNameProvider, "\\usepackage", "\\RequirePackage")
    }

    private fun registerGlossariesCompletion() {
        extendLatexCommands(LatexGlossariesCompletionProvider, CommandMagic.glossaryReference)
    }

    /**
     * Adds support for document classes to the autocomplete.
     */
    private fun registerDocumentClassCompletion() = extendLatexCommands(
        LatexDocumentclassProvider,
        "\\documentclass", "\\LoadClass", "\\LoadClassWithOptions"
    )

    /**
     * Adds autocompletion for parameters that have been given a default [Argument.Type].
     * When the type contains a completion contributor, it will be registered to the correct6 argument.
     */
    private fun registerLatexArgumentTypeCompletion() = Argument.Type.values().forEach { type ->
        val completionProvider = type.completionProvider ?: return@forEach
        extend(
            CompletionType.BASIC,
            PlatformPatterns.psiElement()
                .inside(LatexParameterText::class.java)
                .withPattern { psiElement, _ ->
                    val parameter = psiElement.parentOfType(LatexParameter::class) ?: return@withPattern false
                    val psiCommand = parameter.parentOfType(LatexCommands::class) ?: return@withPattern false
                    val command = psiCommand.defaultCommand() ?: return@withPattern false
                    val index = parameter.indexOf() ?: return@withPattern false
                    val argument = command.arguments.getOrNull(index) ?: return@withPattern false
                    argument.type == type
                }
                .withLanguage(LatexLanguage),
            completionProvider
        )
    }

    /**
     * Adds default environment names to the autocompletion.
     */
    private fun registerDefaultEnvironmentCompletion() = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(LatexRequiredParam::class.java)
            .inside(PlatformPatterns.or(PlatformPatterns.psiElement(LatexBeginCommand::class.java), PlatformPatterns.psiElement(LatexEndCommand::class.java)))
            .withLanguage(LatexLanguage),
        LatexCommandsAndEnvironmentsCompletionProvider(LatexMode.ENVIRONMENT_NAME)
    )

    /**
     * Adds a completion contributor that gets activated within the first required parameter of a given set of commands.
     */
    private fun extendLatexCommands(
        provider: CompletionProvider<CompletionParameters>,
        vararg commandNamesWithSlash: String
    ) = extendLatexCommands(provider, setOf(*commandNamesWithSlash))

    /**
     * Adds a completion contributor that gets activated within the first required parameter of a given set of commands.
     */
    private fun extendLatexCommands(
        provider: CompletionProvider<CompletionParameters>,
        commandNamesWithSlash: Set<String>
    ) = extend(
        CompletionType.BASIC,
        PlatformPatterns.psiElement()
            .inside(LatexParameterText::class.java)
            .inside(LatexRequiredParam::class.java)
            .withPattern { psiElement, _ ->
                val command = psiElement.parentOfType(LatexCommands::class) ?: return@withPattern false
                if (command.commandToken.text in commandNamesWithSlash) return@withPattern true

                CommandManager.updateAliases(commandNamesWithSlash, psiElement.project)
                CommandManager.getAliases(command.commandToken.text).intersect(commandNamesWithSlash).isNotEmpty()
            }
            .withLanguage(LatexLanguage),
        provider
    )
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.ProcessingContext
import com.intellij.util.containers.ContainerUtil
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.completion.handlers.MoveToEndOfCommandHandler
import nl.hannahsten.texifyidea.util.findAvailableDocumentClasses

/**
 * @author Hannah Schellekens
 */
object LatexDocumentclassProvider : CompletionProvider<CompletionParameters>() {

    /**
     * List of all available default documentclasses.
     */
    private val DEFAULT_CLASSES = setOf(
        "article", "IEEEtran", "proc", "report", "book", "slides", "memoir", "letter", "beamer"
    )

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val project = parameters.editor.project ?: return
        val classes = DEFAULT_CLASSES + project.findAvailableDocumentClasses()
        result.addAllElements(
            ContainerUtil.map2List(classes) { name ->
                LookupElementBuilder.create(name, name)
                    .withPresentableText(name)
                    .bold()
                    .withIcon(TexifyIcons.DOT_CLASS)
                    .withInsertHandler(MoveToEndOfCommandHandler)
            }
        )
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.indexing.FileBasedIndex
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.index.file.LatexExternalEnvironmentIndex
import nl.hannahsten.texifyidea.lang.*
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.Kindness
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import java.util.*

/**
 * Provide environments for the autocompletion.
 */
object LatexEnvironmentProvider {

    fun addIndexedEnvironments(result: CompletionResultSet, parameters: CompletionParameters) {
        val project = parameters.editor.project ?: return

        val usesTexlive = LatexCommandsAndEnvironmentsCompletionProvider.isTexliveAvailable
        val packagesInProject = if (!usesTexlive) emptyList() else includedPackages(LatexIncludesIndex.getItems(project), project).plus(
            LatexPackage.DEFAULT
        )

        result.addAllElements(
            FileBasedIndex.getInstance().getAllKeys(LatexExternalEnvironmentIndex.id, project)
                .flatMap { envText ->
                    Environment.lookupInIndex(envText, project)
                        .filter { if (usesTexlive) it.dependency in packagesInProject else true }
                        .map { env ->
                            createEnvironmentLookupElement(env)
                        }
                }
        )
    }

    private fun createEnvironmentLookupElement(env: Environment): LookupElementBuilder {
        return LookupElementBuilder.create(env, env.environmentName)
            .withPresentableText(env.environmentName)
            .bold()
            .withTailText(env.getArgumentsDisplay() + " " + packageName(env), true)
            .withIcon(TexifyIcons.DOT_ENVIRONMENT)
    }

    fun addEnvironments(result: CompletionResultSet, parameters: CompletionParameters) {
        // Find all environments.
        val environments: MutableList<Environment> = ArrayList()
        Collections.addAll(environments, *DefaultEnvironment.values())
        LatexDefinitionIndex.getItemsInFileSet(parameters.originalFile).stream()
            .map { it as LatexCommands }
            .filter { cmd -> CommandMagic.environmentDefinitions.contains(cmd.name) }
            .map { cmd -> cmd.requiredParameter(0) }
            .filter { obj -> Objects.nonNull(obj) }
            .map { environmentName -> SimpleEnvironment(environmentName!!) }
            .forEach { e: SimpleEnvironment -> environments.add(e) }

        // Create autocomplete elements.
        result.addAllElements(
            ContainerUtil.map2List(environments) { env: Environment ->
                createEnvironmentLookupElement(env)
            }
        )
        result.addLookupAdvertisement(Kindness.getKindWords())
    }

    fun packageName(dependend: Dependend): String {
        val name = dependend.dependency.name
        return if ("" == name) {
            ""
        }
        else " ($name)"
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.ProcessingContext
import nl.hannahsten.texifyidea.completion.handlers.MoveToEndOfCommandHandler
import nl.hannahsten.texifyidea.index.LatexGlossaryEntryIndex
import nl.hannahsten.texifyidea.lang.commands.LatexGlossariesCommand.*
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexRequiredParam
import nl.hannahsten.texifyidea.psi.LatexStrictKeyValPair
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.parser.requiredParameters
import nl.hannahsten.texifyidea.util.parser.toStringMap

object LatexGlossariesCompletionProvider : CompletionProvider<CompletionParameters>() {

    private fun getOptionsMap(pairs: List<LatexStrictKeyValPair>): LinkedHashMap<String, String> {
        return pairs.associate { pair -> Pair(pair.keyValKey, pair.keyValValue) }.toStringMap()
    }

    private fun prettyPrintParameter(param: LatexRequiredParam): String {
        return if (param.requiredParamContentList.isNotEmpty()) {
            param.requiredParamContentList.joinToString { c -> c.text }
        }
        else {
            param.strictKeyValPairList.joinToString { p -> p.text }
        }
    }

    private fun buildLookupElement(command: LatexCommands, label: String, short: String, long: String) =
        LookupElementBuilder.create(label, label)
            .withPsiElement(command)
            .withPresentableText(label)
            .withTailText(" ${long.replace("\n", " ")}", true)
            .withTypeText(short)
            .bold()
            .withInsertHandler(MoveToEndOfCommandHandler)

    override fun addCompletions(
        parameters: CompletionParameters,
        context: ProcessingContext,
        result: CompletionResultSet
    ) {
        val glossaryCommands = LatexGlossaryEntryIndex.getItemsInFileSet(parameters.originalFile)
        val lookupItems = glossaryCommands.mapNotNull { command: LatexCommands ->
            when (command.name) {
                NEWACRONYM.cmd, NEWABBREVIATION.cmd -> {
                    val params = command.getRequiredParameters()
                    val label = params.getOrNull(0) ?: return@mapNotNull null
                    val short = params.getOrNull(1) ?: return@mapNotNull null
                    val description = command.requiredParameters().getOrNull(2) ?: return@mapNotNull null
                    buildLookupElement(command, label, short, prettyPrintParameter(description))
                }
                NEWGLOSSARYENTRY.cmd -> {
                    val label = command.getRequiredParameters().getOrNull(0) ?: return@mapNotNull null
                    val options =
                        command.requiredParameters().getOrNull(1)?.strictKeyValPairList ?: return@mapNotNull null
                    val optionsMap = getOptionsMap(options)
                    val short = optionsMap.getOrDefault("name", "")
                    val description = optionsMap.getOrDefault("description", "")
                    buildLookupElement(command, label, short, description)
                }
                LONGNEWGLOSSARYENTRY.cmd -> {
                    val label = command.getRequiredParameters().getOrNull(0) ?: return@mapNotNull null
                    val options =
                        command.requiredParameters().getOrNull(1)?.strictKeyValPairList ?: return@mapNotNull null
                    val optionsMap = getOptionsMap(options)
                    val description = command.requiredParameters().getOrNull(2) ?: return@mapNotNull null
                    val short = optionsMap.getOrDefault("name", "")
                    buildLookupElement(command, label, short, prettyPrintParameter(description))
                }
                else -> {
                    null
                }
            }
        }
        result.addAllElements(lookupItems)
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.ProcessingContext
import com.intellij.util.containers.ContainerUtil
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens
 */
object LatexListTypeProvider : CompletionProvider<CompletionParameters>() {

    /**
     * List of all available default list types.
     */
    private val DEFAULT_LIST_TYPES = setOf("itemize", "enumerate", "description")

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        result.addAllElements(
            ContainerUtil.map2List(DEFAULT_LIST_TYPES) { name ->
                LookupElementBuilder.create(name, name)
                    .withPresentableText(name)
                    .bold()
                    .withIcon(TexifyIcons.MISCELLANEOUS_ITEM)
            }
        )
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.PlatformIcons
import com.intellij.util.ProcessingContext
import nl.hannahsten.texifyidea.lang.magic.DefaultMagicKeys

/**
 * @author Hannah Schellekens
 */
object LatexMagicCommentKeyProvider : CompletionProvider<CompletionParameters>() {

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val keys = DefaultMagicKeys.values()
        result.addAllElements(
            keys.map {
                LookupElementBuilder.create(it, it.displayKey)
                    .withCaseSensitivity(false)
                    .withPresentableText(it.key)
                    .bold()
                    .withIcon(PlatformIcons.PROTECTED_ICON)
                    .withInsertHandler(InsertHandler)
            }
        )
    }

    /**
     * @author Hannah Schellekens
     */
    object InsertHandler : com.intellij.codeInsight.completion.InsertHandler<LookupElement> {

        override fun handleInsert(context: InsertionContext, item: LookupElement) {
            val editor = context.editor
            val document = editor.document
            val caret = editor.caretModel
            val offset = caret.offset

            // Only add the "=" when we are not completing the "fake" magic comment.
            val postFix = if (item.lookupString == DefaultMagicKeys.FAKE.displayKey) " " else " = "
            document.insertString(offset, postFix)
            caret.moveToOffset(offset + postFix.length)
        }
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.ProcessingContext
import javax.swing.Icon

class LatexMagicCommentValueProvider(private val prefixRegex: Regex, val values: HashSet<String>, val icon: Icon? = null) :
    CompletionProvider<CompletionParameters>() {

    override fun addCompletions(
        parameters: CompletionParameters,
        context: ProcessingContext,
        result: CompletionResultSet
    ) {
        // Because it was too difficult to parse elements of the magic comment separately,
        // the default prefix matcher takes the complete comment as prefix (including the key, e.g., suppress =)
        // so we override the prefix to remove that manually.
        val prefix = prefixRegex.replaceFirst(result.prefixMatcher.prefix, "")

        result.withPrefixMatcher(prefix)
            .addAllElements(
                values.map {
                    LookupElementBuilder.create(it)
                        .bold()
                        .withIcon(icon)
                }
            )
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.util.ProcessingContext
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.completion.handlers.MoveToEndOfCommandHandler
import nl.hannahsten.texifyidea.util.PackageUtils

/**
 * @author Hannah Schellekens
 */
object LatexPackageNameProvider : CompletionProvider<CompletionParameters>() {

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val packageNames = PackageUtils.CTAN_PACKAGE_NAMES.toMutableSet()

        result.addAllElements(
            packageNames.map { name ->
                LookupElementBuilder.create(name, name)
                    .withPresentableText(name)
                    .bold()
                    .withIcon(TexifyIcons.STYLE_FILE)
                    .withInsertHandler(MoveToEndOfCommandHandler)
            }
        )
    }
}

package nl.hannahsten.texifyidea.completion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.psi.PsiFile
import com.intellij.util.ProcessingContext
import com.intellij.util.ui.ColorIcon
import nl.hannahsten.texifyidea.gutter.LatexElementColorProvider
import nl.hannahsten.texifyidea.util.Kindness
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import nl.hannahsten.texifyidea.util.getCommandsInFiles
import nl.hannahsten.texifyidea.util.parser.getRequiredArgumentValueByName
import nl.hannahsten.texifyidea.util.parser.isColorDefinition
import nl.hannahsten.texifyidea.util.magic.ColorMagic
import java.awt.Color

object LatexXColorProvider : CompletionProvider<CompletionParameters>() {

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        addDefaultColors(result)
        addCustomColors(parameters, result)
    }

    private fun addDefaultColors(result: CompletionResultSet) {
        result.addAllElements(
            ColorMagic.defaultXcolors.map {
                LookupElementBuilder.create(it.key)
                    .withIcon(ColorIcon(12, Color(it.value)))
            }
        )
    }

    private fun addCustomColors(parameters: CompletionParameters, result: CompletionResultSet) {
        val file = parameters.originalFile
        val files: MutableSet<PsiFile> = HashSet(file.referencedFileSet())
        val cmds = getCommandsInFiles(files, file)
        for (cmd in cmds) {
            if (!cmd.isColorDefinition()) {
                continue
            }

            val colorName = cmd.getRequiredArgumentValueByName("name") ?: continue
            val color = LatexElementColorProvider().findColor(colorName, file)
            val lookupElement = if (color != null) {
                LookupElementBuilder.create(colorName).withIcon(ColorIcon(12, color))
            }
            else LookupElementBuilder.create(colorName)
            result.addElement(lookupElement)
        }
        result.addLookupAdvertisement(Kindness.getKindWords())
    }
}

package nl.hannahsten.texifyidea.documentation

import com.intellij.lang.documentation.DocumentationProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.lang.Described
import nl.hannahsten.texifyidea.psi.BibtexKey

/**
 * @author Hannah Schellekens
 */
open class BibtexDocumentationProvider : DocumentationProvider {

    /**
     * The currently active lookup item.
     */
    private var lookup: Described? = null

    override fun getUrlFor(p0: PsiElement?, p1: PsiElement?): MutableList<String> = ArrayList()

    override fun getQuickNavigateInfo(element: PsiElement, originalPsiElement: PsiElement) = when (element) {
        is BibtexKey -> StringDeclarationLabel(element).makeLabel()
        else -> null
    }

    override fun getDocumentationElementForLookupItem(manager: PsiManager?, obj: Any?, element: PsiElement?): PsiElement? {
        if (obj == null || obj !is Described) {
            return null
        }

        lookup = obj
        return element
    }

    override fun generateDoc(element: PsiElement, element2: PsiElement?) = lookup?.description

    override fun getDocumentationElementForLink(manager: PsiManager, string: String, element: PsiElement): PsiElement? = null
}

package nl.hannahsten.texifyidea.documentation

import nl.hannahsten.texifyidea.psi.BibtexEntry

/**
 * @author Hannah Schellekens, Sten Wessel
 */
open class IdDeclarationLabel(val id: BibtexEntry) : NavigationLabel<BibtexEntry>(id) {

    override fun makeLabel(): String {
        val identifier = id.name
        return String.format("Go to declaration of bibliography item '%s' [%s:%d]", identifier, fileName(), lineNumber())
    }
}

package nl.hannahsten.texifyidea.documentation

import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * @author Hannah Schellekens, Sten Wessel
 */
open class LabelDeclarationLabel(val command: LatexCommands) : NavigationLabel<LatexCommands>(command) {

    override fun makeLabel(): String {
        if ("\\label" == command.name) {
            val label = command.requiredParameter(0)
            return String.format("Go to declaration of label '%s' [%s:%d]", label, fileName(), lineNumber())
        }

        return "Unkown label"
    }
}

package nl.hannahsten.texifyidea.documentation

import arrow.core.Either
import arrow.core.raise.either
import com.intellij.lang.documentation.DocumentationProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.CommandFailure
import nl.hannahsten.texifyidea.lang.Dependend
import nl.hannahsten.texifyidea.lang.Described
import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.settings.sdk.TexliveSdk
import nl.hannahsten.texifyidea.util.SystemEnvironment
import nl.hannahsten.texifyidea.util.containsAny
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.*
import nl.hannahsten.texifyidea.util.runCommandWithExitCode

/**
 * @author Sten Wessel
 */
class LatexDocumentationProvider : DocumentationProvider {

    /**
     * The currently active lookup item.
     */
    private var lookup: Described? = null

    override fun getQuickNavigateInfo(psiElement: PsiElement, originalElement: PsiElement) = when (psiElement) {
        is LatexCommands -> LabelDeclarationLabel(psiElement).makeLabel()
        is BibtexEntry -> IdDeclarationLabel(psiElement).makeLabel()
        else -> null
    }

    /**
     * Check if this is a command which includes a package, in which case we should show docs for the package that is included instead of the command itself.
     */
    private fun isPackageInclusionCommand(element: PsiElement?): Boolean {
        if (element !is LatexCommands) {
            return false
        }

        val command = LatexCommand.lookup(element)
        if (command.isNullOrEmpty()) return false
        return command.first().commandWithSlash in CommandMagic.packageInclusionCommands
    }

    override fun getUrlFor(element: PsiElement?, originalElement: PsiElement?): List<String>? {
        return getUrlForElement(element).getOrNull()
    }

    private fun getUrlForElement(element: PsiElement?): Either<CommandFailure, List<String>?> = either {
        if (element !is LatexCommands) {
            return@either null
        }

        val command = LatexCommand.lookup(element)

        if (command.isNullOrEmpty()) return@either null

        // Special case for package inclusion commands
        if (isPackageInclusionCommand(element)) {
            val pkg = element.getRequiredParameters().getOrNull(0) ?: return@either null
            return runTexdoc(LatexPackage(pkg))
        }

        return runTexdoc(command.first().dependency)
    }

    /**
     * Generate the content that should be shown in the documentation popup.
     */
    private fun generateDocForBibtexId(element: BibtexId): String? {
        val entry = element.parentsOfType(BibtexEntry::class).firstOrNull() ?: return null
        fun formatAuthor(author: String): String {
            val parts = author.split(",")
            if (parts.size < 2) return author
            val last = parts[1].trim()
            val first = parts[0].trim()
            return "$first $last"
        }

        val stringBuilder = StringBuilder("
${entry.getTitle()} (${entry.getYear()})
")
        stringBuilder.append(entry.getAuthors().joinToString(", ") { a -> formatAuthor(a) })
        stringBuilder.append("

")
        stringBuilder.append(entry.getAbstract())
        return stringBuilder.toString()
    }

    /**
     * Generate the content that should be shown in the documentation popup.
     * Works for commands and environments
     */
    private fun generateDocForLatexCommandsAndEnvironments(element: PsiElement, originalElement: PsiElement?): String? {
        // Indexed documentation
        if (lookup == null) {
            // Apparently the lookup item is not yet initialised, so let's do that first
            // Can happen when requesting documentation for an item for which the user didn't request documentation during autocompletion
            if (element is LatexCommands) {
                lookup = LatexCommand.lookup(element)?.firstOrNull()
            }
            // If the cursor is on the parameter text inside a begin/end command, we show docs for the environment
            else if (element is LatexParameterText && (element.parentOfType(LatexBeginCommand::class) != null || element.parentOfType(LatexEndCommand::class) != null)) {
                val envName = element.text
                lookup = Environment[envName] ?: Environment.lookupInIndex(envName, element.project).firstOrNull()
            }
        }
        var docString = if (lookup != null) lookup?.description else ""

        // Link to package docs
        originalElement ?: return null
        val urlsMaybe = if (lookup is Dependend && !isPackageInclusionCommand(element)) runTexdoc((lookup as? Dependend)?.dependency) else getUrlForElement(
            element
        )
        val urlsText = urlsMaybe.fold(
            { it.output },
            { urls -> urls?.joinToString(separator = "
") { "$it" } }
        )

        // Add a line break if necessary
        if (docString?.isNotBlank() == true && urlsText?.isNotBlank() == true) {
            docString += "
"
        }

        docString += urlsText

        if (element.previousSiblingIgnoreWhitespace() == null) {
            lookup = null
        }

        // If we return a blank string, the popup will just say "Fetching documentation..."
        return if (docString.isNullOrBlank()) "
" else docString
    }

    // originalElement: element under the mouse cursor
    // element: element to which originalElement resolves
    override fun generateDoc(element: PsiElement, originalElement: PsiElement?): String? {
        // We resolve to the bibtex id (not the entry), see LatexLabelParameterReference
        if (element is BibtexId) {
            return generateDocForBibtexId(element)
        }
        return generateDocForLatexCommandsAndEnvironments(element, originalElement)
    }

    override fun getDocumentationElementForLookupItem(
        psiManager: PsiManager?,
        obj: Any?,
        psiElement: PsiElement?
    ): PsiElement? {
        if (obj == null || obj !is Described) {
            // Cancel documentation popup
            lookup = null
            return null
        }

        lookup = obj
        return psiElement
    }

    override fun getDocumentationElementForLink(
        psiManager: PsiManager?,
        s: String?,
        psiElement: PsiElement?
    ): PsiElement? = null

    /**
     * Find list of documentation urls.
     */
    private fun runTexdoc(pkg: LatexPackage?): Either<CommandFailure, List<String>> = either {
        if (pkg == null) return@either emptyList()

        // base/lt... files are documented in source2e.pdf
        val name = if (pkg.fileName.isBlank() || (pkg.name.isBlank() && pkg.fileName.startsWith("lt"))) "source2e" else pkg.fileName

        val command = if (TexliveSdk.isAvailable) {
            // -M to avoid texdoc asking to choose from the list
            listOf("texdoc", "-l", "-M", name)
        }
        else {
            if (SystemEnvironment.isAvailable("texdoc")) {
                // texdoc on MiKTeX is just a shortcut for mthelp which doesn't need the -M option
                listOf("texdoc", "-l", name)
            }
            else {
                // In some cases, texdoc may not be available but mthelp is
                listOf("mthelp", "-l", name)
            }
        }
        val (output, exitCode) = runCommandWithExitCode(*command.toTypedArray(), returnExceptionMessage = true)
        if (exitCode != 0 || output?.isNotBlank() != true) {
            raise(CommandFailure(output ?: "", exitCode))
        }

        // Assume that if there are no path delimiters in the output, the output is some sort of error message (could be in any language)
        val validLines = output.split("\n").filter { it.containsAny(setOf("\\", "/")) }

        if (validLines.isEmpty()) {
            raise(CommandFailure(output, exitCode))
        }

        validLines.toSet().mapNotNull {
            // Do some guesswork about the format
            if (TexliveSdk.isAvailable) {
                // Line consists of: name version path optional file description
                it.split("\t").getOrNull(2)
            }
            else {
                // mthelp seems to just output the paths itself
                it
            }
        }
    }
}

package nl.hannahsten.texifyidea.documentation

import com.intellij.lang.parameterInfo.CreateParameterInfoContext
import com.intellij.lang.parameterInfo.ParameterInfoHandler
import com.intellij.lang.parameterInfo.ParameterInfoUIContext
import com.intellij.lang.parameterInfo.UpdateParameterInfoContext
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * This class appears to have something to do with parameter info, but no idea what it does.
 *
 * @author Sten Wessel
 */
class LatexParameterInfoHandler : ParameterInfoHandler<LatexCommands, LatexCommand> {

    private fun findLatexCommand(file: PsiFile, offset: Int): LatexCommands? {
        val element = file.findElementAt(offset)
        return PsiTreeUtil.getParentOfType(element, LatexCommands::class.java)
    }

    override fun findElementForParameterInfo(context: CreateParameterInfoContext): LatexCommands? {
        return findLatexCommand(context.file, context.offset)
    }

    override fun showParameterInfo(element: LatexCommands, context: CreateParameterInfoContext) {
        val commands = LatexRegularCommand[element.commandToken.text.substring(1)] ?: return

        context.itemsToShow = commands.toTypedArray()
        context.showHint(element, element.textOffset, this)
    }

    override fun findElementForUpdatingParameterInfo(context: UpdateParameterInfoContext): LatexCommands? {
        return findLatexCommand(context.file, context.offset)
    }

    override fun updateParameterInfo(element: LatexCommands, context: UpdateParameterInfoContext) {
        context.setCurrentParameter(0)
    }

    override fun updateUI(cmd: LatexCommand?, context: ParameterInfoUIContext) {
        if (cmd == null) {
            context.isUIComponentEnabled = false
            return
        }

        val index = context.currentParameterIndex
        val arguments = cmd.arguments

        if (index >= arguments.size) {
            context.isUIComponentEnabled = false
            return
        }

        context.setupUIComponentPresentation(cmd.commandWithSlash + cmd.getArgumentsDisplay(), 0, 0, false, false, true, context.defaultParameterColor)
    }
}

package nl.hannahsten.texifyidea.documentation

import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement

/**
 * @author Hannah Schellekens
 */
abstract class NavigationLabel<out Psi : PsiElement>(val element: Psi) {

    abstract fun makeLabel(): String

    fun file() = element.containingFile!!

    fun fileName() = file().name

    fun lineNumber(): Int = 1 + StringUtil.offsetToLineNumber(
        file().text,
        element.textOffset
    ) // Because line numbers do start at 1
}

package nl.hannahsten.texifyidea.documentation

import nl.hannahsten.texifyidea.psi.BibtexKey

/**
 * @author Hannah Schellekens, Sten Wessel
 */
open class StringDeclarationLabel(val key: BibtexKey) : NavigationLabel<BibtexKey>(key) {

    override fun makeLabel(): String {
        val identifier = key.text
        return String.format("Go to declaration of string '%s' [line %d]", identifier, lineNumber())
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.openapi.util.Condition
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEndCommand
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.parser.firstParentOfType

/**
 * Disable WordSelectioner (on by default) for LatexCommands, because we handle those in [LatexCommandSelectioner].
 */
class CommandSelectionFilter : Condition<PsiElement> {

    override fun value(t: PsiElement?) = !(
        TexifySettings.getInstance().includeBackslashInSelection && (
            t?.firstParentOfType(LatexCommands::class)?.text == t?.text ||
                t?.firstParentOfType(LatexBeginCommand::class)?.textOffset == t?.textOffset ||
                t?.firstParentOfType(LatexEndCommand::class)?.textOffset == t?.textOffset
            )
        )
}

package nl.hannahsten.texifyidea.editor

import com.intellij.openapi.editor.CustomFileDropHandler
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.action.wizard.graphic.InsertGraphicWizardAction
import nl.hannahsten.texifyidea.util.files.extractFile
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.magic.FileMagic
import java.awt.datatransfer.Transferable
import java.io.File
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class GraphicsDragAndDropHandler : CustomFileDropHandler() {

    /**
     * Whether the file can be dropped using this handler.
     */
    private fun File.isDroppable(): Boolean {
        return extension.lowercase(Locale.getDefault()) in FileMagic.graphicFileExtensions
    }

    override fun canHandle(transferable: Transferable, editor: Editor?): Boolean {
        // Only target dropped files.
        val project = editor?.project ?: return false
        val file = transferable.extractFile() ?: return false

        // Only handle files droppable by the handler.
        if (file.isDroppable().not()) return false

        // Only allow dropping in LaTeX sources.
        return editor.document.psiFile(project)?.isLatexFile() == true
    }

    override fun handleDrop(transferable: Transferable, editor: Editor?, project: Project?): Boolean {
        if (editor == null) return false
        if (project == null) return false

        val toDrop = transferable.extractFile() ?: return false
        editor.document.psiFile(project)?.virtualFile?.let { file ->
            InsertGraphicWizardAction(toDrop).executeAction(file, project)
        }

        return true
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.ide.PasteProvider
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.editor.actions.PasteAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.action.wizard.graphic.InsertGraphicWizardAction
import nl.hannahsten.texifyidea.file.SaveImageFromClipboardDialog
import nl.hannahsten.texifyidea.util.files.extractFile
import nl.hannahsten.texifyidea.util.files.isLatexFile
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.Transferable

/**
 * When pasting image files: do the same as dragging and dropping an image file.
 * When pasting images: save the image.
 *
 * @author Hannah Schellekens
 */
open class ImagePasteProvider : PasteProvider {

    override fun performPaste(dataContext: DataContext) {
        val project = dataContext.getData(PlatformDataKeys.PROJECT) ?: return
        val file = dataContext.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return
        val clipboardTransferable = dataContext.getData(PasteAction.TRANSFERABLE_PROVIDER)?.produce() ?: return

        // When pasting some copied image.
        if (SaveImageFromClipboardDialog.supportsImage(clipboardTransferable)) {
            pasteRawImage(project, file, clipboardTransferable)
        }
        // When pasting an existing image file.
        else if (clipboardTransferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
            pasteImageFile(project, file, clipboardTransferable)
        }
    }

    private fun pasteRawImage(project: Project, file: VirtualFile, clipboard: Transferable) {
        SaveImageFromClipboardDialog(project, clipboard) {
            it.savedImage?.let { imageFile ->
                InsertGraphicWizardAction(imageFile).executeAction(file, project)
            }
        }
    }

    private fun pasteImageFile(project: Project, file: VirtualFile, clipboard: Transferable) {
        val clipboardFile = clipboard.extractFile() ?: return
        InsertGraphicWizardAction(clipboardFile).executeAction(file, project)
    }

    override fun isPastePossible(dataContext: DataContext): Boolean {
        val file = dataContext.getData(PlatformDataKeys.PSI_FILE) ?: return false
        if (file.isLatexFile().not()) return false

        val transferable = dataContext.getData(PasteAction.TRANSFERABLE_PROVIDER)?.produce() ?: return false
        return SaveImageFromClipboardDialog.supportsImage(transferable) ||
            transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
    }

    override fun isPasteEnabled(dataContext: DataContext) = isPastePossible(dataContext)

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.editor

import com.intellij.codeInsight.editorActions.ExtendWordSelectionHandlerBase
import com.intellij.codeInsight.editorActions.SelectWordUtil
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement

/**
 * Select all of the LatexCommands, so including the backslash.
 */
class LatexCommandSelectioner : ExtendWordSelectionHandlerBase() {

    override fun canSelect(e: PsiElement): Boolean {
        return !CommandSelectionFilter().value(e)
    }

    override fun select(e: PsiElement, editorText: CharSequence, cursorOffset: Int, editor: Editor): MutableList<TextRange>? {
        val ranges = super.select(e, editorText, cursorOffset, editor) ?: return null
        val commandRange = e.textRange

        SelectWordUtil.addWordOrLexemeSelection(false, editor, cursorOffset, mutableListOf(commandRange)) { c: Char -> c.isLetterOrDigit() || c == '\\' }
        ranges += commandRange
        return ranges
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.lang.Commenter

/**
 * @author Sten Wessel
 */
open class LatexCommenter : Commenter {

    override fun getLineCommentPrefix() = "%"

    override fun getBlockCommentPrefix(): String? = null

    override fun getBlockCommentSuffix(): String? = null

    override fun getCommentedBlockCommentPrefix(): String? = null

    override fun getCommentedBlockCommentSuffix(): String? = null
}

package nl.hannahsten.texifyidea.editor

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzerSettings
import com.intellij.codeInsight.daemon.LineMarkerInfo
import com.intellij.codeInsight.daemon.LineMarkerProvider
import com.intellij.codeInsight.daemon.impl.LineMarkersPass
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * Provides line markers for the LaTeX language.
 *
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexLineMarkerProvider : LineMarkerProvider {

    override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
        // Method separators before sectioning commands
        if (!DaemonCodeAnalyzerSettings.getInstance().SHOW_METHOD_SEPARATORS || element !is LatexCommands) return null

        val colourManager = EditorColorsManager.getInstance()
        val commandToken = element.commandToken.text
        return if (commandToken in CommandMagic.sectionMarkers) {
            LineMarkersPass.createMethodSeparatorLineMarker(element.commandToken, colourManager).apply {
                separatorColor = CommandMagic.sectionSeparatorColors[commandToken]
            }
        }
        else null
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.PsiAwareDefaultLineWrapPositionStrategy
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexRawText
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.parser.firstParentOfType

/**
 * Based on JavaLineWrapPositionStrategy.
 *
 * @author Thomas
 */
class LatexLineWrapStrategy : PsiAwareDefaultLineWrapPositionStrategy(true, *enabledTypes) {

    companion object {

        // The types that are allowed to be wrapped.
        // (I think only lowest level elements count, unless a parent element contains only one child?)
        val enabledTypes = arrayOf(
            LatexTypes.COMMENT_TOKEN,
            LatexTypes.REQUIRED_PARAM_CONTENT,
            LatexTypes.NORMAL_TEXT_WORD,
            LatexTypes.RAW_TEXT_TOKEN,
        )
    }

    override fun doCalculateWrapPosition(
        document: Document,
        project: Project?,
        element: PsiElement,
        startOffset: Int,
        endOffset: Int,
        maxPreferredOffset: Int,
        isSoftWrap: Boolean
    ): Int {
        val wrapPosition = super.doCalculateWrapPosition(document, project, element, startOffset, endOffset, maxPreferredOffset, isSoftWrap)

        // Don't wrap urls, both in comments and in \url-like commands
        return if (element is PsiComment) {
            getWrapPositionInComment(element, wrapPosition)
        }
        else if (element.node.elementType == LatexTypes.RAW_TEXT_TOKEN) {
            getWrapPositionInRawText(element, wrapPosition)
        }
        else {
            return wrapPosition
        }
    }

    private fun String.findUrl(): Int {
        @Suppress("HttpUrlsUsage")
        return this.indexOfAny(setOf("http://", "https://", "ftp://", "file://", "mailto:"))
    }

    /**
     * Don't wrap urls in comments.
     */
    private fun getWrapPositionInComment(
        element: PsiElement,
        defaultWrapPosition: Int
    ): Int {
        val text = element.text
        val urlStartInText = text.findUrl()
        if (urlStartInText == -1) return defaultWrapPosition

        // Best guess at url end in comments
        val url = text.subSequence(urlStartInText, text.length).takeWhile { it.isWhitespace().not() }
        val urlEnd = element.startOffset + urlStartInText + url.length
        val isInUrl = defaultWrapPosition in element.startOffset + urlStartInText..urlEnd
        if (!isInUrl) return defaultWrapPosition

        // Don't wrap if there is nothing to wrap
        if (text.substring(urlStartInText + url.length).isBlank()) return NO_ELEMENT_WRAP

        // We can still wrap after the url if there is non-blank normal text
        return urlEnd
    }

    /**
     * Don't wrap urls in \url-like commands.
     */
    private fun getWrapPositionInRawText(
        element: PsiElement,
        defaultWrapPosition: Int
    ): Int {
        // In case of the argument of \href and \url, we are given a leaf element (not the RAW_TEXT element) so we need to find the whole url
        val elementWithUrl = element.firstParentOfType(LatexRawText::class) ?: return defaultWrapPosition
        val urlIndex = elementWithUrl.text.findUrl()
        if (urlIndex == -1 && defaultWrapPosition != -1) return defaultWrapPosition

        // We don't wrap in the middle of the command, but we could also wrap in the text description of \href
        val urlEnd = elementWithUrl.firstParentOfType(LatexCommands::class)?.endOffset
        return urlEnd ?: defaultWrapPosition
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.formatting.FormatConstants
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LanguageLineWrapPositionStrategy
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.util.TextRange
import com.intellij.util.MathUtil
import com.intellij.util.text.CharArrayUtil
import nl.hannahsten.texifyidea.util.lineIndentationByOffset
import java.awt.Font
import kotlin.math.max
import kotlin.math.min

/**
 * This class has most code copied from EditorFacade, which has internal API status.
 */
object LatexLineWrapper {

    fun doWrapLongLinesIfNecessary(
        editor: Editor, document: Document, startOffset: Int,
        endOffset: Int, enabledRanges: List<TextRange>, rightMargin: Int
    ) {
        // Normalization.
        val startOffsetToUse = MathUtil.clamp(startOffset, 0, document.textLength)
        var endOffsetToUse = MathUtil.clamp(endOffset, 0, document.textLength)
        val strategy = LanguageLineWrapPositionStrategy.INSTANCE.forEditor(editor)
        val text = document.charsSequence
        val startLine = document.getLineNumber(startOffsetToUse)
        val endLine = document.getLineNumber(0.coerceAtLeast(endOffsetToUse - 1))
        var maxLine = document.lineCount.coerceAtMost(endLine + 1)
        var tabSize = EditorUtil.getTabSize(editor)
        if (tabSize <= 0) {
            tabSize = 1
        }
        val spaceSize = EditorUtil.getSpaceWidth(Font.PLAIN, editor)
        val shifts = mutableListOf(0, 0)
        // shifts[0] - lines shift.
        // shift[1] - offset shift.
        var cumulativeShift = 0

        // This is a for loop from line = startLine until maxLine (but Kotlin)
        var line = startLine - 1
        while (line < maxLine - 1) {
            line++

            val startLineOffset = document.getLineStartOffset(line)
            val endLineOffset = document.getLineEndOffset(line)
            if (!canWrapLine(
                    startOffsetToUse.coerceAtLeast(startLineOffset),
                    endOffsetToUse.coerceAtMost(endLineOffset),
                    cumulativeShift,
                    enabledRanges
                )
            ) {
                continue
            }
            val preferredWrapPosition = calculatePreferredWrapPosition(
                editor,
                text,
                tabSize,
                spaceSize,
                startLineOffset,
                endLineOffset,
                endOffsetToUse,
                rightMargin
            )
            if (preferredWrapPosition < 0 || preferredWrapPosition >= endLineOffset) {
                continue
            }
            if (preferredWrapPosition >= endOffsetToUse) {
                return
            }

            // We know that current line exceeds right margin if control flow reaches this place, so, wrap it.
            val wrapOffset = strategy.calculateWrapPosition(
                document, editor.project, max(startLineOffset, startOffsetToUse), min(endLineOffset, endOffsetToUse),
                preferredWrapPosition, false, false
            )
            if (wrapOffset < 0 // No appropriate wrap position is found.
                // No point in splitting line when its left part contains only white spaces, example:
                //    line start -> |                   | <- right margin
                //                  |   aaaaaaaaaaaaaaaa|aaaaaaaaaaaaaaaaaaaa() <- don't want to wrap this line even if it exceeds right margin
                || CharArrayUtil.shiftBackward(text, startLineOffset, wrapOffset - 1, " \t") < startLineOffset
            ) {
                continue
            }

            // Move caret to the target position and emulate pressing <enter>.
            editor.caretModel.moveToOffset(wrapOffset)
            emulateEnter(editor, shifts)

            // If number of inserted symbols on new line after wrapping more or equal then symbols left on previous line
            // there was no point to wrapping it, so reverting to before wrapping version
            if (shifts[1] - 1 >= wrapOffset - startLineOffset) {
                document.deleteString(wrapOffset, wrapOffset + shifts[1])
            }
            else {
                // We know that number of lines is just increased, hence, update the data accordingly.
                maxLine += shifts[0]
                endOffsetToUse += shifts[1]
                cumulativeShift += shifts[1]
            }
        }
    }

    private fun canWrapLine(startOffset: Int, endOffset: Int, offsetShift: Int, enabledRanges: List<TextRange>): Boolean {
        for (range in enabledRanges) {
            if (range.containsOffset(startOffset - offsetShift) && range.containsOffset(endOffset - offsetShift)) return true
        }
        return false
    }

    /**
     * Checks if it's worth to try to wrap target line (it's long enough) and tries to calculate preferred wrap position.
     *
     * @param editor                target editor
     * @param text                  text contained at the given editor
     * @param tabSize               tab space to use (number of visual columns occupied by a tab)
     * @param spaceSize             space width in pixels
     * @param startLineOffset       start offset of the text line to process
     * @param endLineOffset         end offset of the text line to process
     * @param targetRangeEndOffset  target text region's end offset
     * @return negative value if no wrapping should be performed for the target line;
     * preferred wrap position otherwise
     */
    private fun calculatePreferredWrapPosition(
        editor: Editor,
        text: CharSequence,
        tabSize: Int,
        spaceSize: Int,
        startLineOffset: Int,
        endLineOffset: Int,
        targetRangeEndOffset: Int,
        rightMargin: Int
    ): Int {
        var hasTabs = false
        var canOptimize = true
        var hasNonSpaceSymbols = false
        loop@ for (i in startLineOffset until min(endLineOffset, targetRangeEndOffset)) {
            when (text[i]) {
                '\t' -> {
                    hasTabs = true
                    if (hasNonSpaceSymbols) {
                        canOptimize = false
                        break@loop
                    }
                }

                ' ' -> {}
                else -> hasNonSpaceSymbols = true
            }
        }
        val reservedWidthInColumns = FormatConstants.getReservedLineWrapWidthInColumns(editor)
        return if (!hasTabs) {
            wrapPositionForTextWithoutTabs(startLineOffset, endLineOffset, targetRangeEndOffset, reservedWidthInColumns, rightMargin)
        }
        else if (canOptimize) {
            wrapPositionForTabbedTextWithOptimization(
                text, tabSize, startLineOffset, endLineOffset, targetRangeEndOffset,
                reservedWidthInColumns, rightMargin
            )
        }
        else {
            wrapPositionForTabbedTextWithoutOptimization(
                editor, text, spaceSize, startLineOffset, endLineOffset, targetRangeEndOffset,
                reservedWidthInColumns, rightMargin
            )
        }
    }

    private fun wrapPositionForTextWithoutTabs(
        startLineOffset: Int, endLineOffset: Int, targetRangeEndOffset: Int,
        reservedWidthInColumns: Int, rightMargin: Int
    ): Int {
        return if (min(endLineOffset, targetRangeEndOffset) - startLineOffset > rightMargin) {
            startLineOffset + rightMargin - reservedWidthInColumns
        }
        else -1
    }

    private fun wrapPositionForTabbedTextWithOptimization(
        text: CharSequence,
        tabSize: Int,
        startLineOffset: Int,
        endLineOffset: Int,
        targetRangeEndOffset: Int,
        reservedWidthInColumns: Int,
        rightMargin: Int
    ): Int {
        var width = 0
        var symbolWidth: Int
        var result = Int.MAX_VALUE
        var wrapLine = false
        for (i in startLineOffset until min(endLineOffset, targetRangeEndOffset)) {
            val c = text[i]
            symbolWidth = if (c == '\t') tabSize - width % tabSize else 1
            if (width + symbolWidth + reservedWidthInColumns >= rightMargin
                && min(endLineOffset, targetRangeEndOffset) - i >= reservedWidthInColumns
            ) {
                // Remember preferred position.
                result = i - 1
            }
            if (width + symbolWidth >= rightMargin) {
                wrapLine = true
                break
            }
            width += symbolWidth
        }
        return if (wrapLine) result else -1
    }

    private fun wrapPositionForTabbedTextWithoutOptimization(
        editor: Editor,
        text: CharSequence,
        spaceSize: Int,
        startLineOffset: Int,
        endLineOffset: Int,
        targetRangeEndOffset: Int,
        reservedWidthInColumns: Int,
        rightMargin: Int
    ): Int {
        var width = 0
        var x = 0
        var newX: Int
        var symbolWidth: Int
        var result = Int.MAX_VALUE
        var wrapLine = false
        for (i in startLineOffset until min(endLineOffset, targetRangeEndOffset)) {
            val c = text[i]
            if (c == '\t') {
                newX = EditorUtil.nextTabStop(x, editor)
                val diffInPixels = newX - x
                symbolWidth = diffInPixels / spaceSize
                if (diffInPixels % spaceSize > 0) {
                    symbolWidth++
                }
            }
            else {
                newX = x + EditorUtil.charWidth(c, Font.PLAIN, editor)
                symbolWidth = 1
            }
            if (width + symbolWidth + reservedWidthInColumns >= rightMargin
                && min(endLineOffset, targetRangeEndOffset) - i >= reservedWidthInColumns
            ) {
                result = i - 1
            }
            if (width + symbolWidth >= rightMargin) {
                wrapLine = true
                break
            }
            x = newX
            width += symbolWidth
        }
        return if (wrapLine) result else -1
    }

    /**
     * Different from EditorFacade, this does not actually press enter, but wraps lines manually.
     *
     * @param editor       target editor
     * @param shifts       two-elements array which is expected to be filled with the following info:
     *                       1. The first element holds added lines number;
     *                       2. The second element holds added symbols number;
     * @author Thomas
     */
    private fun emulateEnter(editor: Editor, shifts: MutableList<Int>) {
        val caretOffset = editor.caretModel.offset
        val document = editor.document
        val selectionModel = editor.selectionModel
        var startSelectionOffset = 0
        var endSelectionOffset = 0
        val restoreSelection = selectionModel.hasSelection()
        if (restoreSelection) {
            startSelectionOffset = selectionModel.selectionStart
            endSelectionOffset = selectionModel.selectionEnd
            selectionModel.removeSelection()
        }
        val textLengthBeforeWrap = document.textLength
        val lineCountBeforeWrap = document.lineCount

        // CHANGED: Just insert a newline instead of pressing enter
        // Keep the indent, but drop any whitespace that is now handled by the newline
        val trailingWhitespace = document.text.subSequence(caretOffset, document.textLength).takeWhile { it.isWhitespace() }
        val indent = document.lineIndentationByOffset(caretOffset)
        document.replaceString(caretOffset, caretOffset + trailingWhitespace.length, "\n$indent")
        editor.caretModel.moveToOffset(caretOffset - trailingWhitespace.length + indent.length)

        val symbolsDiff = document.textLength - textLengthBeforeWrap
        if (restoreSelection) {
            var newSelectionStart = startSelectionOffset
            var newSelectionEnd = endSelectionOffset
            if (startSelectionOffset >= caretOffset) {
                newSelectionStart += symbolsDiff
            }
            if (endSelectionOffset >= caretOffset) {
                newSelectionEnd += symbolsDiff
            }
            selectionModel.setSelection(newSelectionStart, newSelectionEnd)
        }
        shifts[0] = document.lineCount - lineCountBeforeWrap
        shifts[1] = symbolsDiff
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.codeInsight.editorActions.moveLeftRight.MoveElementLeftRightHandler
import com.intellij.psi.PsiElement
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.psi.LatexCommands

class LatexMoveElementLeftRightHandler : MoveElementLeftRightHandler() {

    override fun getMovableSubElements(element: PsiElement): Array<PsiElement> {
        return (element as? LatexCommands)?.parameterList?.toArray(emptyArray()) ?: emptyArray()
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.application.options.CodeStyle
import com.intellij.codeInsight.editorActions.fillParagraph.ParagraphFillHandler
import com.intellij.formatting.FormatterTagHandler
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.UnfairTextRange
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.nextLeaf
import com.intellij.psi.util.prevLeaf
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexDisplayMath
import nl.hannahsten.texifyidea.psi.LatexEndCommand
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.parser.hasParent
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * Implements the paragraph fill handler action.
 *
 * @author Abby Berkers
 */
class LatexParagraphFillHandler : ParagraphFillHandler() {

    override fun isAvailableForFile(psiFile: PsiFile?): Boolean {
        return psiFile is LatexFile
    }

    /**
     * Given the leaf [element] at the caret at the time of invoking the fill paragraph action, replace the text of the
     * current paragraph with the same text such that the new text fills the editor evenly.
     *
     * Based on the super implementation, which wasn't custom enough. We now have finer control over when an element ends.
     */
    override fun performOnElement(element: PsiElement, editor: Editor) {
        val document = editor.document

        val (textRange, preFix, postFix) = getParagraphTextRange(element)
        if (textRange.isEmpty) return
        val text = textRange.substring(element.containingFile.text)

        val subStrings = StringUtil.split(text, "\n", true)
        // Join the paragraph text to a single line, so it can be wrapped later.
        val replacementText = preFix + subStrings.joinToString(" ") { it.trim() } + postFix

        CommandProcessor.getInstance().executeCommand(element.project, {
            document.replaceString(
                textRange.startOffset, textRange.endOffset,
                replacementText
            )
            val file = element.containingFile
            val formatterTagHandler = FormatterTagHandler(CodeStyle.getSettings(file))
            val enabledRanges = formatterTagHandler.getEnabledRanges(file.node, TextRange.create(0, document.textLength))

            // Don't simulate enter to wrap lines like EditorFacade does, as it has side effects (source: Yann)
            LatexLineWrapper.doWrapLongLinesIfNecessary(
                editor, document, textRange.startOffset,
                textRange.startOffset + replacementText.length + 1,
                enabledRanges,
                CodeStyle.getSettings(file).getRightMargin(element.language)
            )
        }, null, document)
    }

    /**
     * Get the text range of the paragraph of the current element, along with a prefix and postfix.
     *
     * Note that these paragraphs are different from the paragraphs as outputted by TeX. The paragraphs we are dealing
     * with here are the walls of text as they appear in the editor. E.g., these paragraphs are separated by a display
     * math environment, while for TeX a display math environment is part of the paragraph.
     */
    private fun getParagraphTextRange(element: PsiElement): Triple<TextRange, String, String> {
        // The final element of the paragraph.
        var endElement = element
        // The last element is the last element we checked. At the end the last element will be the first element that
        // is not part of the current paragraph.
        var lastElement = element.nextLeaf(true)
        while (lastElement != null && !separatesParagraph(lastElement)) {
            endElement = lastElement
            lastElement = lastElement.nextLeaf(true)
        }
        val postFix = if (endElement.isPostOrPreFix()) endElement.text else ""

        // The first element of the paragraph.
        var startElement = element
        // The last element that we checked. At the end the first element will be the last element that is not part of
        // the current paragraph, i.e., the paragraph starts at the next element after firstElement.
        var firstElement = element.prevLeaf(true)
        while (firstElement != null && !separatesParagraph(firstElement)) {
            startElement = firstElement
            firstElement = firstElement.prevLeaf(true)
        }
        val preFix = if (startElement.isPostOrPreFix()) startElement.text else ""

        return Triple(UnfairTextRange(startElement.startOffset, endElement.endOffset()), preFix, postFix)
    }

    /**
     * A paragraph ends when we encounter
     * - multiple new lines, or
     * - the begin of an environment (display math counts for this as well, inline math does not), or
     * - the end of the file.
     * In that case the current element is the first element that is not part of the paragraph anymore. If the last
     * element before the current element is a white space, that white space is still part of the paragraph.
     */
    private fun separatesParagraph(element: PsiElement): Boolean {
        return when {
            element is PsiWhiteSpace -> element.text.count { it == '\n' } >= 2
            element.hasParent(LatexBeginCommand::class) -> true
            element.hasParent(LatexEndCommand::class) -> true
            element.hasParent(LatexDisplayMath::class) -> true
            element.hasParent(LatexCommands::class) -> {
                CommandMagic.sectionMarkers.contains(element.parentOfType(LatexCommands::class)?.commandToken?.text)
            }
            else -> false
        }
    }

    /**
     * If the first or last element of a paragraph is a white space, this white space should be preserved.
     *
     * For example, when the paragraph is ended by an environment, the `\begin{env}` is the element that separates this
     * paragraph. The last element of the paragraph then is the new line before the `\begin{env}`. White spaces are
     * trimmed when creating the replacement text, but this white space should be preserved.
     */
    private fun PsiElement.isPostOrPreFix() = this is PsiWhiteSpace
}

package nl.hannahsten.texifyidea.editor

import com.intellij.openapi.application.runUndoTransparentWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.lang.DefaultEnvironment
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.parser.endOffset

class MathEnvironmentEditor(
    private val oldEnvironmentName: String,
    private val newEnvironmentName: String,
    val editor: Editor,
    val environment: PsiElement
) {

    /**
     * Apply the conversion of a math environment.
     */
    fun apply() {
        val document = editor.document
        val indent = document.lineIndentationByOffset(environment.textOffset)

        // The number of characters to replace before the begin block of the old environment starts.
        val whitespace = if (newEnvironmentName == "inline") indent.length + 1
        else if (oldEnvironmentName == "inline") 1
        else 0

        // Extra white space to be added to the beginning of the new environment, when converting from/to inline.
        val extraWhiteSpace = if (newEnvironmentName == "inline") {
            // Add indentation if indentation of old environment is bigger than the previous line.
            val indentOfPreviousLine = document.lineIndentation(document.getLineNumber(environment.textOffset) - 1)
            // Get the previous line, so we can check if it ends with a sentence separator. In that case the inline should not move to the previous line.
            val previousLine = document.getText(TextRange(document.getLineStartOffset(document.getLineNumber(environment.textOffset) - 1), environment.textOffset))
            when {
                indentOfPreviousLine.length < indent.length -> "\n$indent"
                PatternMagic.sentenceSeparatorAtLineEnd.matcher(previousLine).find() -> "\n$indent"
                else -> " "
            }
        }
        else if (oldEnvironmentName == "inline") {
            // Add a newline if there is no text on the line before the inline environment.
            val prefixOnLine = document.getText(TextRange(document.getLineStartOffset(document.getLineNumber(environment.textOffset)), environment.textOffset))
            if (prefixOnLine.matches(Regex("^\\s*"))) " " else "\n$indent"
        }
        else ""

        // The number of characters to replace after the end block of the old environment ends.
        val extra = if (newEnvironmentName == "inline") {
            // Only add the indentation if the next line is indented by at least the same amount as the end command
            // of the old environment.
            val nextLineIndent = document.lineIndentation(
                document.getLineNumber(environment.endOffset()) + 1
            )
            if (nextLineIndent.length < indent.length) 0 else indent.length
        }
        else if (oldEnvironmentName == "inline") 1
        else 0

        // Extra new line to be added at the end of the new environment if the old environment was inline.
        val extraNewLine = if (oldEnvironmentName == "inline") {
            // If the rest of the line is empty, add a new line without indentation.
            val restOfLine = document.getText(
                TextRange(
                    environment.endOffset(),
                    document.getLineEndOffset(document.getLineNumber(environment.endOffset()))
                )
            )
            if (restOfLine.matches(Regex("^\\s*"))) "\n" else "\n$indent"
        }
        else ""

        // Convert the body to one line if necessary.
        val originalBody = if (isOneLineEnvironment(newEnvironmentName)) {
            MultiLineMathContentToOneLine(getBody(indent)).getOneLiner()
        }
        else {
            getBody(indent)
        }

        // Deal with the parameter from the alignat environments.
        val body = if (needsParameter(oldEnvironmentName) && !needsParameter(newEnvironmentName)) {
            originalBody.replaceBefore("}", "").removePrefix("}").removePrefix("\n")
        }
        else {
            originalBody
        }

        val newText = extraWhiteSpace + beginBlock(indent) +
            body.replace("\n", "\n$indent    ") +
            endBlock(indent) + extraNewLine

        runUndoTransparentWriteAction {
            document.replaceString(
                environment.textOffset - whitespace,
                environment.endOffset() + extra,
                newText
            )
            // Place caret at the end of math content (works most of the time).
            editor.caretModel.moveToOffset(
                environment.textOffset +
                    newText.length - extra - extraWhiteSpace.length - extraNewLine.length - endBlock(indent).length
            )
            val file = environment.containingFile
                ?: return@runUndoTransparentWriteAction
            if (isAmsMathEnvironment(newEnvironmentName) && LatexPackage.AMSMATH !in file.includedPackages()) {
                file.insertUsepackage(LatexPackage.AMSMATH)
            }
        }
    }

    private fun isAmsMathEnvironment(environmentName: String): Boolean {
        val amsMathEnvironments: Array<String> = DefaultEnvironment.values()
            .filter { it.dependency == LatexPackage.AMSMATH }
            .map { it.environmentName }
            .toTypedArray()
        return amsMathEnvironments.contains(environmentName)
    }

    /**
     * Determines if the environment is a one line environment.
     */
    private fun isOneLineEnvironment(environmentName: String): Boolean {
        return environmentName == "inline" || environmentName == "display" || environmentName == "equation" || environmentName == "equation*"
    }

    private fun needsParameter(environmentName: String): Boolean {
        return environmentName.contains("alignat")
    }

    /**
     * Construct the begin block for an environment.
     * Removes, keeps, or adds the curly braces when the environment needs a parameter.
     */
    private fun beginBlock(indent: String): String = if (needsParameter(newEnvironmentName)) {
        if (needsParameter(oldEnvironmentName)) "\\begin{$newEnvironmentName}"
        else "\\begin{$newEnvironmentName}{}\n$indent    "
    }
    else {
        when (newEnvironmentName) {
            "inline" -> "$"
            "display" -> "\\[\n$indent    "
            else -> "\\begin{$newEnvironmentName}\n$indent    "
        }
    }

    /**
     * Construct the end block for an environment.
     */
    private fun endBlock(indent: String): String = when (newEnvironmentName) {
        "inline" -> "$"
        "display" -> "\n$indent\\]"
        else -> "\n$indent\\end{$newEnvironmentName}"
    }

    /**
     * Remove the begin and end blocks from an environment, keeping only the body regardless of indentation
     * in the editor.
     */
    private fun getBody(indent: String): String = when (oldEnvironmentName) {
        "inline" -> environment.text.trimRange(1, 1).trim()
        "display" -> environment.text.trimRange(2, 2).replace("$indent    ", "").trim()
        else -> {
            environment.text
                .trimRange(
                    "\\begin{}".length + oldEnvironmentName.length,
                    "\\end{}".length + oldEnvironmentName.length
                )
                .replace("$indent    ", "")
                .trim()
        }
    }
}

package nl.hannahsten.texifyidea.editor

import nl.hannahsten.texifyidea.util.magic.PatternMagic

class MultiLineMathContentToOneLine(private val equation: String) {

    fun getOneLiner(): String = reduce()

    /**
     * Reduce the content of a multi line environment to 'one' line.
     *
     * Removes all line breaks and alignment characters, except those inside a cases or split
     * environment. Changes all occurrences of '\intertext{}' to '\text{}'.
     */
    private fun reduce(): String {
        // Split the string at every begin/end command for the cases/split environments.
        val splitByBeginEnd: MutableList<String> = equation.split(PatternMagic.casesOrSplitCommands).toMutableList()

        fun isBeginCommand(text: String): Boolean = text.contains("\\begin")

        fun isEndCommand(text: String): Boolean = text.contains("\\end")

        fun parse(text: String) {
            val index = splitByBeginEnd.indexOf(text)
            splitByBeginEnd[index] = text
                .replace("\n", "") // Remove new lines.
                .replace("\\\\", "") // Remove end-of-line alignment characters.
                .replace("&", "") // Remove alignment characters.
                .replace("\\intertext", "\\text")
        }

        // Only parse text outside of cases and split environments.
        var depth = 0
        splitByBeginEnd.forEach {
            if (!isBeginCommand(it) && !isEndCommand(it) && depth == 0) parse(it)
            else if (isBeginCommand(it)) depth++
            else if (isEndCommand(it)) depth--
        }
        return splitByBeginEnd.joinToString(separator = "")
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.ide.PasteProvider
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.editor.actions.PasteAction
import nl.hannahsten.texifyidea.action.wizard.table.ColumnType
import nl.hannahsten.texifyidea.action.wizard.table.LatexTableWizardAction
import nl.hannahsten.texifyidea.action.wizard.table.TableCreationDialogWrapper
import nl.hannahsten.texifyidea.action.wizard.table.TableCreationTableModel
import nl.hannahsten.texifyidea.util.Clipboard
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.toVector
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.awt.datatransfer.DataFlavor
import java.util.*

/**
 * Pastes the table html into a Insert Table Wizard if applicable.
 *
 * @author Hannah Schellekens
 */
open class TablePasteProvider : PasteProvider {

    override fun performPaste(dataContext: DataContext) {
        val file = dataContext.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return
        val project = dataContext.getData(PlatformDataKeys.PROJECT) ?: return
        val clipboardHtml = dataContext.transferableHtml() ?: return
        val html = Clipboard.extractHtmlFromClipboard(clipboardHtml) ?: return
        val tableDialog = Jsoup.parse(html).toTableDialogWrapper() ?: return

        LatexTableWizardAction().executeAction(file, project, tableDialog)
    }

    override fun isPastePossible(dataContext: DataContext): Boolean {
        val file = dataContext.getData(PlatformDataKeys.PSI_FILE) ?: return false
        if (file.isLatexFile().not()) return false

        val pasteData = dataContext.transferableHtml() ?: return false
        return pasteData.contains("<table", ignoreCase = true)
    }

    override fun isPasteEnabled(dataContext: DataContext) = isPastePossible(dataContext)

    override fun getActionUpdateThread() = ActionUpdateThread.BGT

    /**
     * Extracts the HTML on clipboard if there is HTML on the clipboard.
     */
    private fun DataContext.transferableHtml(): String? {
        val pasteData = getData(PasteAction.TRANSFERABLE_PROVIDER)?.produce() ?: return null
        return if (pasteData.isDataFlavorSupported(DataFlavor.allHtmlFlavor)) {
            pasteData.getTransferData(DataFlavor.allHtmlFlavor) as String
        }
        else null
    }

    /**
     * Creates the Table Creation Dialog filled in with the data from the clipboard.
     */
    @Suppress("USELESS_CAST")
    private fun Document.toTableDialogWrapper(): TableCreationDialogWrapper? {
        val rows = select("table tr")
        val height = rows.size
        val width = rows.firstOrNull()?.select("td, th")?.size ?: 0

        if (height == 0 && width == 0) return null

        // Convert html to data vector Vector<Vector<Any?>> and headers.
        val header = rows.firstOrNull()?.select("td, th")?.mapNotNull { it.text() }?.toVector() ?: return null
        val content: Vector<Vector<Any?>> = rows.drop(1).map { tr ->
            tr.select("td, th").map { td -> td.handleHtmlFormatting() as Any? }.toVector()
        }.toVector()

        // Find the type of column automatically.
        val contentRows = rows.drop(1)
        val columnTypes = (0 until width).map { col ->
            // Check if all contents of the column (except the header) can be converted to a number.
            // When that's the case => it's a number column. All other cases, text. Ignoring the Math option
            // as the table information is most probably something outside of a latex context.
            if (contentRows.all { it.select("td, th").getOrNull(col)?.text()?.toDoubleOrNull() != null }) {
                ColumnType.NUMBERS_COLUMN
            }
            else ColumnType.TEXT_COLUMN
        }

        return TableCreationDialogWrapper(
            columnTypes,
            TableCreationTableModel(content, header)
        )
    }

    /**
     * Converts <b>/<i>/<u> tags to latex formatting commands.
     * `this` is a <td> HTML Element.
     */
    private fun Element.handleHtmlFormatting(): String {
        val prefix = StringBuilder()
        val suffix = StringBuilder()

        if (select("b, strong").isNotEmpty()) {
            prefix.append("\\textbf{")
            suffix.append("}")
        }
        if (select("i, em").isNotEmpty()) {
            prefix.append("\\textit{")
            suffix.append("}")
        }
        if (select("u").isNotEmpty()) {
            prefix.append("\\underline{")
            suffix.append("}")
        }

        return prefix
            .append(text())
            .append(suffix.toString())
            .toString()
    }
}

package nl.hannahsten.texifyidea.file

import com.intellij.extapi.psi.PsiFileBase
import com.intellij.psi.FileViewProvider
import nl.hannahsten.texifyidea.grammar.BibtexLanguage

/**
 * @author Hannah Schellekens
 */
open class BibtexFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, BibtexLanguage) {

    override fun getFileType() = BibtexFileType

    override fun toString() = "BibTeX bibliography file"
}

package nl.hannahsten.texifyidea.file

import com.intellij.openapi.fileTypes.LanguageFileType
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens
 */
object BibtexFileType : LanguageFileType(BibtexLanguage) {

    override fun getName() = "BibTeX bibliography file"

    override fun getDescription() = "BibTeX bibliography file"

    override fun getDefaultExtension() = "bib"

    override fun getIcon() = TexifyIcons.BIBLIOGRAPHY_FILE

    override fun getDisplayName() = name
}

package nl.hannahsten.texifyidea.file

import com.intellij.extapi.psi.PsiFileBase
import com.intellij.psi.FileViewProvider
import nl.hannahsten.texifyidea.grammar.LatexLanguage

/**
 * @author Hannah Schellekens
 */
class ClassFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, LatexLanguage) {

    override fun getFileType() = ClassFileType

    override fun toString() = "LaTeX class file"
}

package nl.hannahsten.texifyidea.file

import com.intellij.openapi.fileTypes.LanguageFileType
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens
 */
object ClassFileType : LanguageFileType(LatexLanguage) {

    override fun getName() = "LaTeX class file"

    override fun getDescription() = "LaTeX class file"

    override fun getDefaultExtension() = "cls"

    override fun getIcon() = TexifyIcons.CLASS_FILE

    override fun getDisplayName() = name
}

package nl.hannahsten.texifyidea.file

/**
 * @author Hannah Schellekens
 */
@FunctionalInterface
interface FileExtensionMatcher {

    /**
     * Checks if the given extension is supported ('matched')  or not..
     *
     * @param extension
     * The extension of the file to match.
     * @return `true` if the extension matches, `false` when the extension does not
     * match.
     */
    fun matchesExtension(extension: String): Boolean
}

package nl.hannahsten.texifyidea.file

/**
 * @author Hannah Schellekens
 */
@FunctionalInterface
interface FileNameMatcher {

    /**
     * Checks if the given file name matches or not.
     *
     * @param fileName
     * The name of the file to match.
     * @return `true` if the fileName matches, `false` when the fileName does not match.
     */
    fun matchesName(fileName: String): Boolean
}

package nl.hannahsten.texifyidea.file

import com.intellij.extapi.psi.PsiFileBase
import com.intellij.psi.FileViewProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import nl.hannahsten.texifyidea.grammar.LatexLanguage

/**
 * @author Sten Wessel
 */
class LatexFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, LatexLanguage), PsiNameIdentifierOwner {

    override fun getFileType() = LatexFileType

    override fun toString() = "LaTeX source file"

    /**
     * Required for refactoring
     */
    override fun getNameIdentifier(): PsiElement {
        return this
    }
}

package nl.hannahsten.texifyidea.file

import com.intellij.openapi.fileTypes.LanguageFileType
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Sten Wessel
 */
object LatexFileType : LanguageFileType(LatexLanguage) {

    override fun getName() = "LaTeX source file"

    override fun getDescription() = "LaTeX source file"

    override fun getDefaultExtension() = "tex"

    override fun getIcon() = TexifyIcons.LATEX_FILE

    // Fix https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/2195#issuecomment-1024344147
    override fun getDisplayName() = name
}

package nl.hannahsten.texifyidea.file

import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.openapi.fileTypes.PlainTextLanguage
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Thomas
 */
object LatexSourceFileType : LanguageFileType(PlainTextLanguage.INSTANCE) {

    override fun getName() = "LaTeX source file (dtx)"

    override fun getDescription() = "LaTeX source file (dtx)"

    override fun getDefaultExtension() = "dtx"

    override fun getIcon() = TexifyIcons.FILE

    override fun getDisplayName() = name
}

package nl.hannahsten.texifyidea.file

import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.*
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.ImageUtil
import com.intellij.util.ui.JBUI
import nl.hannahsten.texifyidea.ui.ImagePanel
import nl.hannahsten.texifyidea.util.Clipboard
import nl.hannahsten.texifyidea.util.addLabeledComponent
import nl.hannahsten.texifyidea.util.formatAsFileName
import org.apache.commons.io.FilenameUtils
import org.jsoup.Jsoup
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.Image
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.Transferable
import java.awt.image.BufferedImage
import java.io.File
import java.util.*
import javax.imageio.ImageIO
import javax.swing.BoxLayout
import javax.swing.JPanel

/**
 * @author Hannah Schellekens
 */
class SaveImageFromClipboardDialog(

    /**
     * The current project.
     */
    val project: Project,

    /**
     * The transferable data from the clipboard.
     * Must support the image data flavor [DataFlavor.imageFlavor].
     */
    private val transferable: Transferable,

    /**
     * The function to execute when the dialog is succesfully closed.
     * Does not execute when cancelled.
     */
    onOkFunction: (SaveImageFromClipboardDialog) -> Unit
) : DialogWrapper(true) {

    companion object {

        fun supportsImage(transferable: Transferable): Boolean {
            return transferable.isDataFlavorSupported(DataFlavor.imageFlavor) && transferable.getTransferData(DataFlavor.imageFlavor) is BufferedImage
        }

        // Default or lasted used resource folder.
        private var resourceFolder: String? = null
    }

    init {
        if (resourceFolder == null) {
            // Use the resource folder by default. If there is no "resources" folder, assume that a non "src/source" folder
            // is a resource folder.
            resourceFolder = ProjectRootManager.getInstance(project).contentSourceRoots.let { roots ->
                roots.firstOrNull { it.nameWithoutExtension == "resources" }?.path
                    ?: roots.firstOrNull { it.nameWithoutExtension.matches(Regex("(src|sources?)", RegexOption.IGNORE_CASE)) }?.path
                    ?: roots.firstOrNull()?.path ?: ""
            }
        }
    }

    /**
     * The image data from the clipboard. Only supports BufferedImages currently, see [supportsImage].
     */
    private val image = transferable.getTransferData(DataFlavor.imageFlavor) as BufferedImage

    /**
     * The width of the image in pixels.
     */
    private val imageWidth = image.width

    /**
     * The height of the image in pixels.
     */
    private val imageHeight = image.height

    /**
     * The image format of the original image, if known.
     */
    private var imageFormat: ImageFormat? = null

    /**
     * The name of the original image, if known.
     */
    private var imageName: String? = null

    /**
     * The image file that was saved, `null` when no image was saved.
     */
    var savedImage: File? = null
        private set

    /**
     * Shows a preview of the image to paste.
     */
    private val imgPreview = ImagePanel().apply {
        val widthRatio = 1.0.coerceAtMost(500.0 / imageWidth)
        val heightRatio = 1.0.coerceAtMost(300.0 / imageHeight)
        val ratio = widthRatio.coerceAtLeast(heightRatio)

        val scaled = image.getScaledInstance((imageWidth * ratio).toInt(), (imageHeight * ratio).toInt(), Image.SCALE_SMOOTH)
        setImage(ImageUtil.toBufferedImage(scaled))
    }

    /**
     * Stores the folder where the image is stored in.
     */
    private val txtResourceFolder = TextFieldWithBrowseButton().apply {
        // resourceFolder is only null before the class is initialised
        text = resourceFolder!!

        addBrowseFolderListener(
            TextBrowseFolderListener(
                FileChooserDescriptor(false, true, false, false, false, false)
                    .withTitle("Select Resource Folder...")
            )
        )
    }

    /**
     * Stores the file name for the image without extension.
     */
    private val txtImageName = JBTextField()

    /**
     * Stores the format of the created image.
     */
    private val cboxImageFormat = ComboBox(ImageFormat.values()).apply {
        selectedItem = ImageFormat.PNG
    }

    init {
        require(transferable.isDataFlavorSupported(DataFlavor.imageFlavor)) { "Image flavor not supported." }

        // Fill in meta data.
        findMetaData()
        imageName?.let { txtImageName.text = it }
        imageFormat?.let { cboxImageFormat.selectedItem = it }

        // Setup dialog.
        super.init()
        title = "Save Image from Clipboard"
        myPreferredFocusedComponent = txtImageName

        if (showAndGet()) {
            saveImage()
            onOkFunction(this@SaveImageFromClipboardDialog)
        }
    }

    override fun createCenterPanel() = JPanel().apply {
        layout = BoxLayout(this, BoxLayout.Y_AXIS)
        minimumSize = Dimension(512, 64)

        addImageMetaPanel()
        addImageDataInputs()
    }

    override fun doOKAction() {
        close(0)
    }

    override fun doValidate() = when {
        txtResourceFolder.text.isBlank() -> {
            ValidationInfo("You must select a destination folder.", txtResourceFolder)
        }
        txtImageName.text.isBlank() -> {
            ValidationInfo("You must enter an image name.")
        }
        txtImageName.text.trim().formatAsFileName().isBlank() -> {
            ValidationInfo("You must enter a valid image name (invalid characters).", txtImageName)
        }
        destinationFile()?.exists() ?: false -> {
            ValidationInfo("This file already exists", txtImageName)
        }
        else -> null
    }

    /**
     * Saves the pasted image.
     */
    private fun saveImage() {
        val imageFormat = cboxImageFormat.selectedItem as? ImageFormat ?: return
        val destination = destinationFile() ?: return

        createDirectories()

        if (ImageIO.write(image, imageFormat.extension, destination)) {
            savedImage = destination
            LocalFileSystem.getInstance().refresh(true)
        }

        // Store user-entered text for next time
        resourceFolder = txtResourceFolder.text
    }

    /**
     * Get the file where the image must be stored.
     * Also creates the necessary directories.
     */
    private fun destinationFile(): File? {
        val imageFormat = cboxImageFormat.selectedItem as? ImageFormat ?: return null

        val directory = File(txtResourceFolder.text)
        val validImageName = txtImageName.text.trim().formatAsFileName()
        val fileName = "$validImageName.${imageFormat.extension}"
        return File("${directory.path}/$fileName")
    }

    /**
     * Creates all necessary directories, if needed.
     */
    private fun createDirectories() {
        val directory = File(txtResourceFolder.text)
        if (directory.exists().not()) {
            directory.mkdirs()
        }
    }

    /**
     * Adds the panel that shows information about the pasted image.
     */
    private fun JPanel.addImageMetaPanel() = add(
        JPanel(BorderLayout(16, 16)).apply {
            border = JBUI.Borders.empty(16)

            add(imgPreview, BorderLayout.WEST)

            // Info labels.
            add(
                JBLabel(
                    """

                    Size:
                    ${this@SaveImageFromClipboardDialog.imageWidth} x ${this@SaveImageFromClipboardDialog.imageHeight} pixels

                    Format:
                    ${imageFormat ?: "unknown"}

                    Name:
                    ${imageName ?: "unknown"}

        """
                )
            )
        }
    )

    /**
     * Adds the user input controls to enter the image information.
     */
    private fun JPanel.addImageDataInputs() {
        val labelWidth = 128
        addLabeledComponent(txtResourceFolder, "Resource folder:", labelWidth)
        addLabeledComponent(txtImageName, "Image name:", labelWidth)
        addLabeledComponent(cboxImageFormat, "Image format:", labelWidth)
    }

    /**
     * Checks if there is HTML data present on the clipboard, and extract meta data of the image from this
     * data when present.
     */
    private fun findMetaData() {
        // Check if there is HTML image metadata present.
        if (transferable.isDataFlavorSupported(DataFlavor.fragmentHtmlFlavor).not()) return

        // Parse clipboard data.
        val clipboardData = transferable.getTransferData(DataFlavor.fragmentHtmlFlavor) as String
        val html = Clipboard.extractHtmlFragmentFromClipboard(clipboardData)?.let {
            Jsoup.parse(it)
        } ?: return
        val image = html.select("img").firstOrNull() ?: return

        // Handle data.
        val source = image.attr("src") ?: return
        imageFormat = ImageFormat.imageFormatFromExtension(FilenameUtils.getExtension(source))
        imageName = FilenameUtils.getBaseName(source)
    }

    /**
     * The image formats that are supported by this dialog.
     *
     * @author Hannah Schellekens
     */
    enum class ImageFormat(val extension: String) {

        PNG("png"),
        JPG("jpg");

        override fun toString() = extension

        companion object {

            fun imageFormatFromExtension(extension: String) = when (extension.lowercase(Locale.getDefault())) {
                "jpg", "jpeg" -> JPG
                else -> PNG
            }
        }
    }
}

package nl.hannahsten.texifyidea.file

import com.intellij.extapi.psi.PsiFileBase
import com.intellij.psi.FileViewProvider
import nl.hannahsten.texifyidea.grammar.LatexLanguage

/**
 * @author Hannah Schellekens
 */
class StyleFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, LatexLanguage) {

    override fun getFileType() = StyleFileType

    override fun toString() = "LaTeX style file"
}

package nl.hannahsten.texifyidea.file

import com.intellij.openapi.fileTypes.LanguageFileType
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens
 */
object StyleFileType : LanguageFileType(LatexLanguage) {

    override fun getName() = "LaTeX style file"

    override fun getDescription() = "LaTeX style file"

    override fun getDefaultExtension() = "sty"

    override fun getIcon() = TexifyIcons.STYLE_FILE

    override fun getDisplayName() = name
}

package nl.hannahsten.texifyidea.file

import com.intellij.extapi.psi.PsiFileBase
import com.intellij.psi.FileViewProvider
import nl.hannahsten.texifyidea.grammar.LatexLanguage

/**
 * @author Thomas Schouten
 */
open class TikzFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, LatexLanguage) {

    override fun getFileType() = TikzFileType

    override fun toString() = "TikZ picture file"
}

package nl.hannahsten.texifyidea.file

import com.intellij.openapi.fileTypes.LanguageFileType
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Thomas Schouten
 */
object TikzFileType : LanguageFileType(LatexLanguage) {

    override fun getName() = "TikZ picture file"

    override fun getDescription() = "TikZ picture file"

    override fun getDefaultExtension() = "tikz"

    override fun getIcon() = TexifyIcons.TIKZ_FILE

    override fun getDisplayName() = name
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.formatting.*
import com.intellij.lang.ASTNode
import com.intellij.psi.TokenType
import com.intellij.psi.formatter.common.AbstractBlock
import nl.hannahsten.texifyidea.psi.BibtexTypes

/**
 * @author Hannah Schellekens
 */
open class BibtexBlock(
    node: ASTNode,
    wrap: Wrap,
    alignment: Alignment?,
    private val spacingBuilder: TexSpacingBuilder
) : AbstractBlock(node, wrap, alignment) {

    override fun buildChildren(): MutableList<Block> {
        val blocks = ArrayList<Block>()
        var child = myNode.firstChildNode

        while (child != null) {
            if (child.elementType != TokenType.WHITE_SPACE) {
                val block = BibtexBlock(
                    child,
                    Wrap.createWrap(WrapType.NONE, false),
                    null,
                    spacingBuilder
                )
                blocks.add(block)
            }

            child = child.treeNext
        }

        return blocks
    }

    override fun getSpacing(child1: Block?, child2: Block) = spacingBuilder.getSpacing(this, child1, child2)

    override fun isLeaf() = myNode.firstChildNode == null

    override fun getIndent(): Indent? {
        val type = myNode.elementType

        // Prevent close brace indent.
        if (type == BibtexTypes.PREAMBLE) {
            return Indent.getNormalIndent(false)
        }
        // Indents in entries.
        if (type == BibtexTypes.ENTRY_CONTENT) {
            return Indent.getNormalIndent(true)
        }

        return Indent.getNoneIndent()
    }

    override fun getChildAttributes(newChildIndex: Int): ChildAttributes {
        val type = myNode.elementType

        if (type == BibtexTypes.ENTRY) {
            return ChildAttributes(Indent.getNormalIndent(true), null)
        }

        return ChildAttributes(Indent.getNoneIndent(), null)
    }
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.formatting.*
import com.intellij.lang.ASTNode
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile

/**
 * @author Hannah Schellekens
 */
open class BibtexFormattingModelBuilder : FormattingModelBuilder {

    override fun createModel(context: FormattingContext) = FormattingModelProvider.createFormattingModelForPsiFile(
        context.containingFile,
        BibtexBlock(
            context.node,
            Wrap.createWrap(WrapType.NONE, false),
            Alignment.createAlignment(),
            createBibtexSpacingBuilder(context.codeStyleSettings)
        ),
        context.codeStyleSettings
    )!!

    override fun getRangeAffectingIndent(file: PsiFile?, offset: Int, elementAtOffset: ASTNode?): TextRange? = null
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.formatting.Spacing
import com.intellij.psi.codeStyle.CodeStyleSettings
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.psi.BibtexTypes.*

fun createBibtexSpacingBuilder(settings: CodeStyleSettings): TexSpacingBuilder {
    val bibtexCommonSettings = settings.getCommonSettings(BibtexLanguage)

    return rules(bibtexCommonSettings) {
        simple {
            around(ASSIGNMENT).spaces(1)
            before(SEPARATOR).spaces(0)
            between(TYPE, OPEN_BRACE).spaces(0)
            between(TYPE, OPEN_PARENTHESIS).spaces(0)
            between(OPEN_BRACE, ID).spaces(0)
            after(OPEN_PARENTHESIS).spaces(1)
            around(CONCATENATE).spaces(1)
            between(ENTRY_CONTENT, ENDTRY).spaces(1)
            before(ENTRY).blankLines(1)
        }

        custom {
            // Only insert a space between two actual words, and leave braces alone as they may appear as groups inside words
            customRule { _, left, right ->
                return@customRule if (right.node?.elementType === NORMAL_TEXT_WORD && left.node?.elementType === NORMAL_TEXT_WORD) {
                    if (left.node?.text in setOf("{", "}") || right.node?.text in setOf("}", "{")) null
                    else Spacing.createSpacing(1, 1, 0, bibtexCommonSettings.KEEP_LINE_BREAKS, bibtexCommonSettings.KEEP_BLANK_LINES_IN_CODE)
                }
                else null
            }
        }
    }
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.application.options.CodeStyle
import com.intellij.formatting.*
import com.intellij.lang.ASTNode
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.TokenType
import com.intellij.psi.formatter.common.AbstractBlock
import com.intellij.psi.util.prevLeaf
import nl.hannahsten.texifyidea.editor.typedhandlers.LatexEnterHandler
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.settings.codestyle.LatexCodeStyleSettings
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.cmd
import java.lang.Integer.max

/**
 * @author Sten Wessel
 *
 * @param sectionIndent Number of extra indents needed because of section indenting.
 */
class LatexBlock(
    node: ASTNode,
    wrap: Wrap?,
    alignment: Alignment?,
    private val spacingBuilder: TexSpacingBuilder,
    private val wrappingStrategy: LatexWrappingStrategy,
    val sectionIndent: Int = 0,
    /** Extra section indent that's not real but is used in case blocks do not start on a new line, so that it is ignored for the enter handler. */
    private val fakeSectionIndent: Int = 0,
) : AbstractBlock(node, wrap, alignment) {

    override fun buildChildren(): List<Block> {
        val blocks = mutableListOf<LatexBlock>()
        var child = myNode.firstChildNode

        // Only applicable for section indenting:
        // Current sectioning level while walking through the file
        // Uses levels of CommandMagic.labeledLevels
        var sectionLevel = -2
        // Extra indent to do because of sectioning
        var extraSectionIndent = max(sectionIndent - 1, 0)

        // If a block does not start on a new line the indent won't do anything and we need to do something else to get the text in the block indented
        val blockToIndentDoesNotStartOnNewLine = myNode.psi is LatexNoMathContent && sectionIndent > 0 && myNode.psi.prevLeaf(false)?.text?.contains("\n") == false

        // Sorry, it's magic
        val newFakeSectionIndent = if (blockToIndentDoesNotStartOnNewLine) sectionIndent + 1 else max(fakeSectionIndent - 1, 0)
        // (I think what it does is that it propagates the section indent to normal text words, and since it only does
        // something for things that come right after a new line the next normal text word on a new line will actually
        // be indented (and probably not so for other structures, but we have to choose a finite number >= 2 here))

        // For section indenting only: add fake blocks at the end because we can only add one indent per block but we may need multiple if inside e.g. subsubsection
        if (child == null && (sectionIndent > 0 || fakeSectionIndent > 0)) {
            val block = LatexBlock(
                myNode,
                wrappingStrategy.getWrap(),
                null,
                spacingBuilder,
                wrappingStrategy,
                extraSectionIndent,
                newFakeSectionIndent
            )
            blocks.add(block)
        }

        // Create child blocks
        while (child != null) {
            val isSectionCommand =
                child.psi is LatexNoMathContent && child.psi.firstChildOfType(LatexCommands::class)?.name in CommandMagic.sectioningCommands.map { it.cmd }

            var targetIndent = extraSectionIndent

            if (isSectionCommand) {
                updateSectionIndent(sectionLevel, extraSectionIndent, targetIndent, child).apply {
                    sectionLevel = first
                    extraSectionIndent = second
                    targetIndent = third
                }
            }

            // Normal addition of blocks
            if (child.elementType !== TokenType.WHITE_SPACE && child !is PsiWhiteSpace) {
                val block = LatexBlock(
                    child,
                    wrappingStrategy.getWrap(),
                    null,
                    spacingBuilder,
                    wrappingStrategy,
                    targetIndent,
                    newFakeSectionIndent
                )
                blocks.add(block)
            }
            child = child.treeNext
        }
        return blocks
    }

    /**
     * Update the current section indent based on the current child.
     * Only applicable when indenting text in sections.
     *
     * @return sectionLevel, extraSectionIndent, targetIndent
     */
    private fun updateSectionIndent(givenSectionLevel: Int, givenExtraSectionIndent: Int, givenTargetIndent: Int, child: ASTNode): Triple<Int, Int, Int> {
        var extraSectionIndent = givenExtraSectionIndent
        var sectionLevel = givenSectionLevel
        var targetIndent = givenTargetIndent

        // Set flag for next blocks until section end to get indent+1
        // We need to do it this way because we cannot create blocks which span a section content: blocks
        // need to correspond to only one psi element.
        // Changing the parser to view section content as one element is problematic because then we need to hardcode the sectioning structure in the parser
        val command = LatexCommand.lookup(child.psi.firstChildOfType(LatexCommands::class)?.name)?.first()
        val level = CommandMagic.labeledLevels[command]
        if (level != null && level > sectionLevel) {
            extraSectionIndent += 1
            sectionLevel = level
        }
        else if (level != null && level < sectionLevel) {
            // I think this will go wrong if you jump levels, e.g. subsubsection after chapter
            // but that's bad style anyway
            extraSectionIndent = max(targetIndent - (sectionLevel - level), 0)
            // Suppose previous text is indented 2 times, and we are a one level higher section command,
            // we need for this command itself an indent of 2, minus one for the level, minus one because the
            // section command itself is indented one less than the text in the section
            // (which will be indented with extraSectionIndent)
            targetIndent = max(targetIndent - (sectionLevel - level) - 1, 0)
            sectionLevel = level
        }
        else if (level != null) {
            // We encounter a same level sectioning command, which should be indented one less
            targetIndent -= 1
        }

        return Triple(sectionLevel, extraSectionIndent, targetIndent)
    }

    override fun getIndent(): Indent? {
        val shouldIndentDocumentEnvironment = CodeStyle.getCustomSettings(node.psi.containingFile, LatexCodeStyleSettings::class.java).INDENT_DOCUMENT_ENVIRONMENT
        val shouldIndentEnvironment = myNode.elementType === LatexTypes.ENVIRONMENT_CONTENT && (
            (myNode.psi as LatexEnvironmentContent)
                .firstParentOfType(LatexEnvironment::class)
                ?.firstChildOfType(LatexBeginCommand::class)
                ?.firstChildOfType(LatexParameterText::class)?.text != "document" || shouldIndentDocumentEnvironment
            )

        if (shouldIndentEnvironment || myNode.elementType === LatexTypes.PSEUDOCODE_BLOCK_CONTENT ||
            // Fix for leading comments inside an environment, because somehow they are not placed inside environments.
            // Note that this does not help to insert the indentation, but at least the indent is not removed
            // when formatting.
            (
                myNode.elementType === LatexTypes.COMMENT_TOKEN &&
                    myNode.treeParent?.elementType === LatexTypes.ENVIRONMENT
                )
        ) {
            return Indent.getNormalIndent(true)
        }

        // Indentation of sections
        val indentSections = CodeStyle.getCustomSettings(node.psi.containingFile, LatexCodeStyleSettings::class.java).INDENT_SECTIONS
        if (indentSections) {
            if (sectionIndent > 0 || fakeSectionIndent > 0) {
                return Indent.getNormalIndent(false)
            }
        }

        // Indentation in groups and parameters.
        if (myNode.elementType === LatexTypes.REQUIRED_PARAM_CONTENT ||
            myNode.elementType === LatexTypes.STRICT_KEY_VAL_PAIR ||
            myNode.elementType === LatexTypes.OPTIONAL_KEY_VAL_PAIR ||
            (
                myNode.elementType !== LatexTypes.CLOSE_BRACE &&
                    myNode.treeParent?.elementType === LatexTypes.GROUP
                ) ||
            (
                myNode.elementType !== LatexTypes.CLOSE_BRACE &&
                    myNode.treeParent?.elementType === LatexTypes.PARAMETER_GROUP
                )
        ) {
            return Indent.getNormalIndent(false)
        }

        // Display math
        return if ((myNode.elementType === LatexTypes.MATH_CONTENT || myNode.elementType === LatexTypes.COMMENT_TOKEN) &&
            myNode.treeParent?.elementType === LatexTypes.DISPLAY_MATH
        ) {
            Indent.getNormalIndent(true)
        }
        else Indent.getNoneIndent()
    }

    override fun getSpacing(child1: Block?, child2: Block): Spacing? {
        return spacingBuilder.getSpacing(this, child1, child2)
    }

    override fun isLeaf(): Boolean {
        return myNode.firstChildNode == null && sectionIndent <= 0
    }

    // Automatic indent when enter is pressed
    override fun getChildAttributes(newChildIndex: Int): ChildAttributes {
        return LatexEnterHandler.getChildAttributes(newChildIndex, node, subBlocks)
    }
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.formatting.*
import com.intellij.lang.ASTNode
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile

/**
 * @author Sten Wessel
 */
class LatexFormattingModelBuilder : FormattingModelBuilder {

    override fun createModel(context: FormattingContext): FormattingModel {
        return FormattingModelProvider.createFormattingModelForPsiFile(
            context.containingFile,
            LatexBlock(
                context.node,
                Wrap.createWrap(WrapType.NONE, false),
                Alignment.createAlignment(),
                createSpacingBuilder(context.codeStyleSettings),
                LatexWrappingStrategy()
            ),
            context.codeStyleSettings
        )
    }

    override fun getRangeAffectingIndent(file: PsiFile, offset: Int, elementAtOffset: ASTNode): TextRange? {
        return null
    }
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.lang.LanguageFormattingRestriction
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic

/**
 * @author Abby Berkers
 *
 * Disable the formatter on files that start and end with a verbatim environment.
 */
class LatexLanguageFormattingRestriction : LanguageFormattingRestriction {

    override fun isFormatterAllowed(context: PsiElement): Boolean = !isFormatterNotAllowed(context)

    private fun isFormatterNotAllowed(context: PsiElement): Boolean {
        return if (context.containingFile is LatexFile) {
            EnvironmentMagic.verbatim.any {
                context.node.text.startsWith("\\begin{$it}") &&
                    context.node.text.endsWith("\\end{$it}")
            }
        }
        else false
    }
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.formatting.Spacing
import com.intellij.psi.codeStyle.CodeStyleSettings
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.formatting.spacingrules.leftTableSpaceAlign
import nl.hannahsten.texifyidea.formatting.spacingrules.rightTableSpaceAlign
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexTypes.*
import nl.hannahsten.texifyidea.settings.codestyle.LatexCodeStyleSettings
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.inDirectEnvironment
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.parentOfType
import java.util.*

fun createSpacing(minSpaces: Int, maxSpaces: Int, minLineFeeds: Int, keepLineBreaks: Boolean, keepBlankLines: Int): Spacing =
    Spacing.createSpacing(minSpaces, maxSpaces, minLineFeeds, keepLineBreaks, keepBlankLines)

/**
 *
 * @author Sten Wessel, Abby Berkers
 */
fun createSpacingBuilder(settings: CodeStyleSettings): TexSpacingBuilder {
    val latexSettings = settings.getCustomSettings(LatexCodeStyleSettings::class.java)
    val latexCommonSettings = settings.getCommonSettings(LatexLanguage)

    return rules(latexCommonSettings) {
        custom {
            customRule { parent, _, right ->
                // Don't insert or remove spaces inside the text in a verbatim environment.
                if (parent.node?.elementType === NORMAL_TEXT) {
                    if (parent.node?.psi?.inDirectEnvironment(EnvironmentMagic.verbatim) == true) {
                        return@customRule Spacing.getReadOnlySpacing()
                    }
                }
                // Don't insert or remove spaces in front of the first word in a verbatim environment.
                else if (right.node?.elementType === ENVIRONMENT_CONTENT) {
                    if (right.node?.psi?.inDirectEnvironment(EnvironmentMagic.verbatim) == true) {
                        return@customRule Spacing.getReadOnlySpacing()
                    }
                }
                return@customRule null
            }
        }

        simple {
            between(NORMAL_TEXT_WORD, NORMAL_TEXT_WORD).spaces(1)
            before(ENVIRONMENT_CONTENT).lineBreakInCode()
            before(PSEUDOCODE_BLOCK_CONTENT).lineBreakInCode()
        }

        // Newline before certain algorithm pseudocode commands
        custom {
            customRule { parent, _, right ->
                // Lowercase to also catch \STATE from algorithmic
                if (right.node?.psi?.firstChildOfType(LatexCommands::class)?.name?.lowercase(Locale.getDefault()) in setOf(
                        "\\state",
                        "\\statex"
                    ) && parent.node?.psi?.inDirectEnvironment(EnvironmentMagic.algorithmEnvironments) == true
                ) {
                    return@customRule Spacing.createSpacing(0, 1, 1, latexCommonSettings.KEEP_LINE_BREAKS, latexCommonSettings.KEEP_BLANK_LINES_IN_CODE)
                }

                return@customRule null
            }
        }

        custom {
            // Insert a new line between the end of environment content and the end command.
            inPosition(parent = ENVIRONMENT, left = ENVIRONMENT_CONTENT, right = END_COMMAND).spacing(
                Spacing.createSpacing(0, Int.MAX_VALUE, 1, latexCommonSettings.KEEP_LINE_BREAKS, latexCommonSettings.KEEP_BLANK_LINES_IN_CODE)
            )
            inPosition(parent = PSEUDOCODE_BLOCK, left = PSEUDOCODE_BLOCK_CONTENT, right = END_PSEUDOCODE_BLOCK).spacing(
                Spacing.createSpacing(0, Int.MAX_VALUE, 1, latexCommonSettings.KEEP_LINE_BREAKS, latexCommonSettings.KEEP_BLANK_LINES_IN_CODE)
            )
        }

        custom {
            fun commentSpacing(minSpaces: Int): Spacing {
                if (latexCommonSettings.KEEP_FIRST_COLUMN_COMMENT) {
                    return Spacing.createKeepingFirstColumnSpacing(minSpaces, Int.MAX_VALUE, latexCommonSettings.KEEP_LINE_BREAKS, latexCommonSettings.KEEP_BLANK_LINES_IN_CODE)
                }
                return createSpacing(minSpaces, Int.MAX_VALUE, 0, latexCommonSettings.KEEP_LINE_BREAKS, latexCommonSettings.KEEP_BLANK_LINES_IN_CODE)
            }

            inPosition(right = COMMENT_TOKEN).spacing(commentSpacing(0))
        }

        custom {
            // Make sure the number of new lines before a sectioning command is as much as the user specified in the settings.
            // BUG OR FEATURE? Does not work for a command that immediately follows \begin{document}.
            customRule { _, _, right ->
                // Because getting the full text from a node is relatively expensive, we first use a condition which is necessary (but not sufficient) as an early exit.
                val commandName = right.node?.psi?.firstChildOfType(LatexCommands::class)?.name
                if (commandName !in LatexCodeStyleSettings.blankLinesOptions.values) return@customRule null

                val rightText = right.node?.text
                LatexCodeStyleSettings.blankLinesOptions.forEach {
                    if (rightText?.matches(Regex("\\\\${it.value.trimStart('\\')}\\{.*}")) == true &&
                        !CommandMagic.definitions.contains(right.node?.psi?.parentOfType(LatexCommands::class)?.name)
                    ) {
                        return@customRule createSpacing(
                            minSpaces = 0,
                            maxSpaces = Int.MAX_VALUE,
                            minLineFeeds = it.key.get(latexSettings) + 1,
                            keepLineBreaks = false,
                            keepBlankLines = 0
                        )
                    }
                }
                return@customRule null
            }
        }

        // Align & in tables.
        // Unfortunately we have to do this manually because Alignment only aligns characters if they are the first
        // non-whitespace in a line of code
        custom {
            customRule { parent, _, right ->
                return@customRule leftTableSpaceAlign(latexCommonSettings, parent, right)
            }
            customRule { parent, left, _ ->
                return@customRule rightTableSpaceAlign(latexCommonSettings, parent, left)
            }
        }
    }
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.formatting.Wrap
import com.intellij.formatting.WrapType

/**
 *
 * @author Sten Wessel
 */
class LatexWrappingStrategy {

    fun getWrap(): Wrap? {
        return Wrap.createWrap(WrapType.NONE, false)
    }
}

package nl.hannahsten.texifyidea.formatting

import com.intellij.formatting.ASTBlock
import com.intellij.formatting.Block
import com.intellij.formatting.Spacing
import com.intellij.formatting.SpacingBuilder
import com.intellij.psi.codeStyle.CommonCodeStyleSettings
import com.intellij.psi.tree.IElementType
import com.intellij.psi.tree.TokenSet

/**
 * Based on KotlinSpacingBuilder in the Kotlin plugin.
 *
 * @author Sten Wessel
 */
class TexSpacingBuilder(private val commonSettings: CommonCodeStyleSettings) {

    private val builders = ArrayList<Builder>()

    /**
     * Generic spacing builder.
     */
    private interface Builder {

        fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing?
    }

    /**
     * Basic spacing builder that is based on the implementation of [SpacingBuilder].
     */
    inner class BasicSpacingBuilder : SpacingBuilder(commonSettings), Builder {

        override fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing? {
            return super.getSpacing(parent, left, right)
        }
    }

    /**
     * Represents a rule condition.
     *
     * The rule is only matched when the condition holds.
     */
    private data class Condition(
        val parent: IElementType? = null,
        val left: IElementType? = null,
        val right: IElementType? = null,
        val parentSet: TokenSet? = null,
        val leftSet: TokenSet? = null,
        val rightSet: TokenSet? = null
    ) : (ASTBlock, ASTBlock, ASTBlock) -> Boolean {

        override fun invoke(p: ASTBlock, l: ASTBlock, r: ASTBlock): Boolean =
            (parent == null || p.node!!.elementType == parent) &&
                (left == null || l.node!!.elementType == left) &&
                (right == null || r.node!!.elementType == right) &&
                (parentSet == null || p.node!!.elementType in parentSet) &&
                (leftSet == null || l.node!!.elementType in leftSet) &&
                (rightSet == null || r.node!!.elementType in rightSet)
    }

    /**
     * Rule that upon matching the conditions returns the spacing action.
     */
    private data class Rule(
        val conditions: List<Condition>,
        val action: (ASTBlock, ASTBlock, ASTBlock) -> Spacing?
    ) : (ASTBlock, ASTBlock, ASTBlock) -> Spacing? {

        override fun invoke(p: ASTBlock, l: ASTBlock, r: ASTBlock): Spacing? =
            if (conditions.all { it(p, l, r) }) action(p, l, r) else null
    }

    /**
     * Build more advanced rules with [Rule] and [Condition] above.
     */
    inner class CustomSpacingBuilder : Builder {

        private val rules = ArrayList<Rule>()
        private var conditions = ArrayList<Condition>()

        override fun getSpacing(parent: ASTBlock, left: ASTBlock, right: ASTBlock): Spacing? {
            for (rule in rules) {
                return rule(parent, left, right) ?: continue
            }

            return null
        }

        fun inPosition(
            parent: IElementType? = null,
            left: IElementType? = null,
            right: IElementType? = null,
            parentSet: TokenSet? = null,
            leftSet: TokenSet? = null,
            rightSet: TokenSet? = null
        ): CustomSpacingBuilder {
            conditions.add(Condition(parent, left, right, parentSet, leftSet, rightSet))
            return this
        }

        fun spacing(spacing: Spacing) {
            newRule { _, _, _ -> spacing }
        }

        fun customRule(block: (parent: ASTBlock, left: ASTBlock, right: ASTBlock) -> Spacing?) {
            newRule(block)
        }

        private fun newRule(rule: (ASTBlock, ASTBlock, ASTBlock) -> Spacing?) {
            val savedConditions = ArrayList(conditions)
            rules.add(Rule(savedConditions, rule))
            conditions.clear()
        }
    }

    /**
     * Get the spacing from the composite builders.
     */
    fun getSpacing(parent: Block, child1: Block?, child2: Block): Spacing? {
        if (parent !is ASTBlock || child1 !is ASTBlock || child2 !is ASTBlock) {
            return null
        }

        for (builder in builders) {
            return builder.getSpacing(parent, child1, child2) ?: continue
        }

        return null
    }

    /**
     * Rules for the basic spacing builder implementation.
     */
    fun simple(init: BasicSpacingBuilder.() -> Unit) {
        val builder = BasicSpacingBuilder()
        builder.init()
        builders.add(builder)
    }

    fun custom(init: CustomSpacingBuilder.() -> Unit) {
        val builder = CustomSpacingBuilder()
        builder.init()
        builders.add(builder)
    }
}

/**
 * Build a [TexSpacingBuilder] with a set of rules.
 */
fun rules(latexSettings: CommonCodeStyleSettings, init: TexSpacingBuilder.() -> Unit): TexSpacingBuilder {
    val builder = TexSpacingBuilder(latexSettings)
    builder.init()
    return builder
}

package nl.hannahsten.texifyidea.grammar

import com.intellij.lang.Language

/**
 * @author Hannah Schellekens
 */
object BibtexLanguage : Language("Bibtex") {

    override fun getDisplayName(): String = "BibTeX"
}

package nl.hannahsten.texifyidea.grammar

import com.intellij.lexer.FlexAdapter

/**
 * @author Hannah Schellekens
 */
open class BibtexLexerAdapter : FlexAdapter(BibtexLexer(null))

package nl.hannahsten.texifyidea.grammar

import com.intellij.lang.ASTNode
import com.intellij.lang.Language
import com.intellij.lang.ParserDefinition
import com.intellij.lang.ParserDefinition.SpaceRequirements
import com.intellij.openapi.project.Project
import com.intellij.psi.FileViewProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.stubs.PsiFileStubImpl
import com.intellij.psi.tree.IStubFileElementType
import com.intellij.psi.tree.TokenSet
import nl.hannahsten.texifyidea.file.BibtexFile
import nl.hannahsten.texifyidea.parser.BibtexParser
import nl.hannahsten.texifyidea.psi.BibtexTypes

/**
 * @author Hannah Schellekens
 */
class BibtexParserDefinition : ParserDefinition {

    override fun createLexer(project: Project) = BibtexLexerAdapter()

    override fun createParser(project: Project) = BibtexParser()

    override fun getFileNodeType(): IStubFileElementType<*> = FILE

    override fun getWhitespaceTokens(): TokenSet = BibtexTokenSets.WHITE_SPACES

    override fun getCommentTokens(): TokenSet = BibtexTokenSets.COMMENTS

    override fun getStringLiteralElements(): TokenSet = BibtexTokenSets.NORMAL_TEXT

    override fun createElement(astNode: ASTNode): PsiElement = BibtexTypes.Factory.createElement(astNode)

    override fun createFile(fileViewProvider: FileViewProvider): PsiFile = BibtexFile(fileViewProvider)

    override fun spaceExistenceTypeBetweenTokens(
        astNode: ASTNode,
        astNode1: ASTNode
    ): SpaceRequirements = SpaceRequirements.MAY

    companion object {

        val FILE = object : IStubFileElementType<BibtexFileStub>(
            "BibtexStubFileElementType", Language.findInstance(BibtexLanguage::class.java)
        ) {
            override fun getStubVersion(): Int = 13
        }
    }

    class BibtexFileStub(file: BibtexFile) : PsiFileStubImpl<BibtexFile>(file)
}

package nl.hannahsten.texifyidea.grammar

import com.intellij.psi.TokenType
import com.intellij.psi.tree.TokenSet
import nl.hannahsten.texifyidea.psi.BibtexTypes

/**
 * See [LatexTokenSets].
 */
interface BibtexTokenSets {

    companion object {

        val WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE)
        val COMMENTS = TokenSet.create(BibtexTypes.COMMENT)
        val NORMAL_TEXT = TokenSet.create(BibtexTypes.NORMAL_TEXT)
    }
}

package nl.hannahsten.texifyidea.grammar

import com.intellij.lang.Language

/**
 * @author Sten Wessel
 */
object LatexLanguage : Language("Latex") {

    override fun getDisplayName(): String {
        return "LaTeX"
    }
}

package nl.hannahsten.texifyidea.grammar

import com.intellij.lexer.FlexAdapter

/**
 * @author Sten Wessel
 */
class LatexLexerAdapter : FlexAdapter(LatexLexer(null))

package nl.hannahsten.texifyidea.grammar

import com.intellij.lang.ASTNode
import com.intellij.lang.Language
import com.intellij.lang.ParserDefinition
import com.intellij.lang.ParserDefinition.SpaceRequirements
import com.intellij.lang.PsiParser
import com.intellij.lexer.Lexer
import com.intellij.openapi.project.Project
import com.intellij.psi.FileViewProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.stubs.PsiFileStubImpl
import com.intellij.psi.tree.IStubFileElementType
import com.intellij.psi.tree.TokenSet
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.parser.LatexParser
import nl.hannahsten.texifyidea.psi.LatexTypes

/**
 * @author Sten Wessel
 */
class LatexParserDefinition : ParserDefinition {

    override fun createLexer(project: Project): Lexer = LatexLexerAdapter()

    override fun createParser(project: Project): PsiParser = LatexParser()

    override fun getFileNodeType(): IStubFileElementType<*> = FILE

    override fun getWhitespaceTokens(): TokenSet = LatexTokenSets.WHITE_SPACES

    override fun getCommentTokens(): TokenSet = LatexTokenSets.COMMENTS

    override fun getStringLiteralElements(): TokenSet = LatexTokenSets.NORMAL_TEXT

    override fun createElement(node: ASTNode): PsiElement = LatexTypes.Factory.createElement(node)

    override fun createFile(viewProvider: FileViewProvider): PsiFile = LatexFile(viewProvider)

    override fun spaceExistenceTypeBetweenTokens(
        left: ASTNode,
        right: ASTNode
    ): SpaceRequirements = SpaceRequirements.MAY

    companion object {

        // debugName is required to let IntelliJ distinguish between this FILE and BibtexParserDefinition.FILE
        val FILE: IStubFileElementType<*> = object : IStubFileElementType<LatexFileStub>(
            "LatexStubFileElementType", Language.findInstance(LatexLanguage::class.java)
        ) {
            override fun getStubVersion(): Int = 58
        }
    }

    class LatexFileStub(file: LatexFile) : PsiFileStubImpl<LatexFile>(file)
}

package nl.hannahsten.texifyidea.grammar

import com.intellij.psi.TokenType
import com.intellij.psi.tree.TokenSet
import nl.hannahsten.texifyidea.psi.LatexTypes

/**
 * Lazy loading of token sets to speed up startup, see https://plugins.jetbrains.com/docs/intellij/lexer-and-parser-definition.html#define-a-parser
 */
interface LatexTokenSets {

    companion object {

        val WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE)
        val COMMENTS = TokenSet.create(LatexTypes.COMMENT_TOKEN)
        val NORMAL_TEXT = TokenSet.create(LatexTypes.NORMAL_TEXT)
    }
}

package nl.hannahsten.texifyidea.gutter

import com.intellij.execution.lineMarker.ExecutorAction
import com.intellij.execution.lineMarker.RunLineMarkerContributor
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.parser.isEntryPoint
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * Puts a run-configuration icon in the gutter in front of the \begin{document} command.
 *
 * @author Hannah Schellekens
 */
class LatexCompileGutter : RunLineMarkerContributor() {

    override fun getInfo(element: PsiElement): Info? {
        // Only show the icon on lines with `\begin` => show only one icon.
        if (element.node.elementType != LatexTypes.BEGIN_TOKEN) return null

        // Find the total enclosed begin command: required to easily find the first command.
        val beginCommand = element.parentOfType(LatexBeginCommand::class) ?: return null

        // Break when not a valid command: don't show icon.
        if (beginCommand.isEntryPoint().not()) return null

        // Lookup actions.
        val actionManager = ActionManager.getInstance()
        val editConfigs = actionManager.getAction("editRunConfigurations")
        val actions = ExecutorAction.getActions(0)

        // Create icon.
        return Info(
            TexifyIcons.BUILD,
            { "Compile document" },
            actions[0],
            editConfigs
        )
    }
}

package nl.hannahsten.texifyidea.gutter

import arrow.core.max
import arrow.core.nonEmptyListOf
import com.intellij.openapi.editor.ElementColorProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.source.tree.LeafPsiElement
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.lang.commands.LatexColorDefinitionCommand
import nl.hannahsten.texifyidea.lang.commands.RequiredArgument
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.magic.ColorMagic
import nl.hannahsten.texifyidea.util.parser.*
import java.awt.Color
import java.util.*
import kotlin.math.max
import kotlin.math.min

/**
 * Provides colors in the gutter.
 *
 * @author Abby
 */
class LatexElementColorProvider : ElementColorProvider {

    /**
     * Set the color in the document based on changes in the color picker from the gutter.
     *
     * Only changes the color when we are in the gutter of a color definition. Do nothing when we are in the gutter
     * of a color usage.
     */
    override fun setColorTo(element: PsiElement, color: Color) {
        if (element is LeafPsiElement) {
            val command = element.firstParentOfType(LatexCommands::class)
            val commandTemplate = LatexColorDefinitionCommand.values().firstOrNull {
                it.commandWithSlash == command?.name
            } ?: return
            val colorModel = command?.getRequiredArgumentValueByName("model-list") ?: return
            val oldColor = command.getRequiredArgumentValueByName("spec-list") ?: return
            val newColorString = when (colorModel.lowercase(Locale.getDefault())) {
                "rgb" -> color.toRgbString(integer = oldColor.split(",").firstOrNull()?.contains('.') == false)
                "hsb" -> color.toHsbString()
                "html" -> color.toHtmlStsring()
                "gray" -> color.toGrayString()
                "cmyk" -> color.toCmykString()
                "cmy" -> color.toCmyString()
                else -> null
            } ?: return

            val colorArgumentIndex =
                commandTemplate.arguments.filterIsInstance<RequiredArgument>().indexOfFirst { it.name == "spec-list" }
            if (colorArgumentIndex == -1) return

            val newColorParameter = LatexPsiHelper(element.project).createRequiredParameter(newColorString)
            val oldColorParameter = command.requiredParameters()[colorArgumentIndex]
            oldColorParameter.parent.node.replaceChild(oldColorParameter.node, newColorParameter.node)
        }
    }

    /**
     * Get the color that is used in a command that uses color. This color will be shown in the gutter.
     */
    override fun getColorFrom(element: PsiElement): Color? {
        if (element is LeafPsiElement) {
            val command = element.firstParentOfType(LatexCommands::class)
            if (command.usesColor()) {
                val colorArgument = when (command?.name?.substring(1)) {
                    // Show the defined color.
                    in ColorMagic.colorDefinitions.map { it.command } -> {
                        command?.getRequiredArgumentValueByName("name")
                    }
                    // Show the used color.
                    in ColorMagic.takeColorCommands -> {
                        command?.getRequiredArgumentValueByName("color")
                    }
                    else -> null
                } ?: return null
                // Find the color to show.
                if (command?.name == element.text) {
                    return findColor(colorArgument, element.containingFile)
                }
            }
        }
        return null
    }

    fun findColor(colorName: String, file: PsiFile): Color? {
        val defaultHex = ColorMagic.defaultXcolors[colorName]

        return if (defaultHex != null) Color(defaultHex)
        else {
            val colorDefiningCommands = LatexCommandsIndex.getCommandsByNames(
                file,
                *ColorMagic.colorDefinitions.map { "\\${it.command}" }
                    .toTypedArray()
            )
            // If this color is a single color (not a mix, and thus does not contain a !)
            // and we did not find it in the default colors (above), it should be in the
            // first parameter of a color definition command. If not, we can not find the
            // color (and return null in the end).
            if (colorName.contains('!') || colorDefiningCommands.map { it.getRequiredArgumentValueByName("name") }
                .contains(colorName)
            ) {
                val colorDefinitionCommand =
                    colorDefiningCommands.find { it.getRequiredArgumentValueByName("name") == colorName }
                when (colorDefinitionCommand?.name?.substring(1)) {
                    LatexColorDefinitionCommand.COLORLET.command -> {
                        getColorFromColorParameter(file, colorDefinitionCommand.getRequiredArgumentValueByName("color"))
                    }
                    LatexColorDefinitionCommand.DEFINECOLOR.command, LatexColorDefinitionCommand.PROVIDECOLOR.command -> {
                        getColorFromDefineColor(
                            colorDefinitionCommand.getRequiredArgumentValueByName("model-list"),
                            colorDefinitionCommand.getRequiredArgumentValueByName("spec-list")
                        )
                    }
                    LatexColorDefinitionCommand.DEFINECOLORSERIES.command -> {
                        getColorFromDefineColor(
                            colorDefinitionCommand.getOptionalArgumentValueByName("b-model") ?: colorDefinitionCommand.getRequiredArgumentValueByName("core model"),
                            colorDefinitionCommand.getRequiredArgumentValueByName("b-spec")
                        )
                    }
                    else -> getColorFromColorParameter(file, colorName)
                }
            }
            else return null
        }
    }

    /**
     * Given the color parameter [definitionText] of a command, compute the defined color.
     */
    private fun getColorFromColorParameter(file: PsiFile, definitionText: String?): Color? {
        definitionText ?: return null
        val colorParts = definitionText.split("!").filter { it.isNotBlank() }
        val colors = colorParts.filter { it.all { c -> c.isLetter() } }
            .map { findColor(it, file) ?: return null }
        if (colors.isEmpty()) return null
        val numbers = colorParts.filter { it.all { c -> c.isDigit() } }
            .map { it.toInt() }
        var currentColor = colors.first()
        for ((i, color) in colors.withIndex()) {
            if (i > 0 && i - 1 in numbers.indices) currentColor = mix(currentColor, color, numbers[i - 1])
        }
        return currentColor
    }

    /**
     * Given the `model-list` and `spec-list` arguments of the \definecolor command,
     * get the corresponding color.
     */
    private fun getColorFromDefineColor(modelText: String?, specText: String?): Color? {
        modelText ?: return null
        specText ?: return null
        return try {
            when (modelText.lowercase(Locale.getDefault())) {
                "rgb" -> fromRgbString(specText)
                "hsb" -> fromHsbString(specText)
                "cmy" -> fromCmyString(specText)
                "cmyk" -> fromCmykString(specText)
                "gray" -> fromGrayString(specText)
                "html" -> fromHtmlString(specText)
                else -> null
            }
        }
        // Exception occurs after typing a comma in an argument, as then we'd
        // try to format an empty string as a number.
        catch (e: NumberFormatException) {
            null
        }
        // Exception occurs when not enough color arguments have been typed.
        // E.g. we need three arguments (r, g, b) and have typed "255, 127".
        catch (e: IndexOutOfBoundsException) {
            null
        }
    }

    /*
     * Methods to parse the argument strings that define colors. Formula's taken
     * from the xcolor documentation.
     */

    /**
     * Mix two colors, used to support red!50!yellow color definitions.
     */
    private fun mix(a: Color, b: Color, percent: Int): Color {
        return (percent / 100.0).let {
            Color(
                (a.red * it + b.red * (1.0 - it)).toInt(),
                (a.green * it + b.green * (1.0 - it)).toInt(),
                (a.blue * it + b.blue * (1.0 - it)).toInt()
            )
        }
    }

    /**
     * Get the [Color] from an RGB string, where the RGB values are either
     * - integers in the range [0, 255],
     * - or floats in the range [0, 1].
     */
    private fun fromRgbString(rgbText: String): Color {
        val rgb = rgbText.split(",").map { it.trim() }
        return try {
            rgb.map { it.toInt().projectOnto(0..255) }.let { Color(it[0], it[1], it[2]) }
        }
        catch (e: NumberFormatException) {
            rgb.map { it.toFloat().projectOnto(0..1) }.let { Color(it[0], it[1], it[2]) }
        }
    }

    /**
     * Convert a [Color] object to an RGB string "R, G, B" with R, G, and B integers in the range [0, 255] if [integer]
     * is true, and R, G, and B floats in the range [0, 1] otherwise.
     */
    private fun Color.toRgbString(integer: Boolean = true): String =
        if (integer) "$red, $green, $blue"
        else listOf(red, green, blue).map { it / 255.0 }.joinToString(", ") { it.format() }

    /**
     * Get the [Color] from an HSB string, assuming that the values are in the range [0, 1].
     */
    private fun fromHsbString(hsbText: String): Color {
        val hsb = hsbText.split(",").map { it.trim() }
        return hsb.map { it.toFloat().projectOnto(0..1) }
            .let {
                Color.getHSBColor(
                    it[0], it[1], it[2]
                )
            }
    }

    /**
     * Convert a color to an HSB string "hue, saturation, brightness" where each value is a float in the range [0, 1].
     */
    private fun Color.toHsbString(): String = Color.RGBtoHSB(red, green, blue, null)
        .joinToString(", ") { it.toDouble().format() }

    /**
     * Get a [Color] object from a cmyk (cyan, magenta, blue, black) string.
     */
    private fun fromCmykString(cmykText: String): Color {
        val cmyk = cmykText.split(",").map { it.trim() }
            .map { it.toFloat() }
        return cmyk.take(3)
            .map { (255 * (1 - cmyk.last()) * (1 - it)).toInt() }
            .map { it.projectOnto(0..255) }
            .let { Color(it[0], it[1], it[2]) }
    }

    /**
     * Convert a [Color] object to a cmyk string.
     */
    private fun Color.toCmykString(): String {
        val rgb = nonEmptyListOf(red, green, blue).map { it / 255.0 }
        val k: Double = 1.0 - rgb.max()
        return rgb.map { (1.0 - it - k) / (1.0 - k) }.joinToString(", ") { it.format() } + ", $k"
    }

    /**
     * Get a [Color] from a cmy string.
     */
    private fun fromCmyString(cmyText: String): Color {
        val cmy = cmyText.split(",")
            .map { it.trim() }
            .map { it.toFloat().projectOnto(0..1) }
        return Color(1 - cmy[0], 1 - cmy[1], 1 - cmy[2])
    }

    /**
     * Convert a [Color] to a cmy string.
     */
    private fun Color.toCmyString() = listOf(red, green, blue)
        .map { 1.0 - (it / 255.0) }.joinToString(", ") { it.format() }

    /**
     * Convert a gray string (i.e., one number taken from the interval [0, 1]) to a [Color].
     */
    private fun fromGrayString(grayText: String): Color {
        fun Float.toRgb() = (this * 255).toInt()
        val gray = grayText.toFloat().projectOnto(0..255)
        return Color(gray.toRgb(), gray.toRgb(), gray.toRgb())
    }

    /**
     * Get a grayscale number from a [Color] object.
     *
     * When the color itself is not gray, it is converted to grayscale by using weights for each color vector [`[1]`](https://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale).
     *
     * 1. [https://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale](https://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale)
     */
    private fun Color.toGrayString() = listOf(0.2126, 0.7152, 0.0722)
        .zip(listOf(red, green, blue))
        .sumOf { (weight, rgb): Pair<Double, Int> -> weight * (rgb / 255.0) }
        .format()

    /**
     * Get a [Color] from a hex color string.
     */
    private fun fromHtmlString(htmlText: String): Color {
        return Color.decode("#$htmlText")
    }

    /**
     * Get the hex string of a [Color], without leading #.
     */
    private fun Color.toHtmlStsring() = "${red.toHexString()}${green.toHexString()}${blue.toHexString()}"

    /**
     * Project [this] onto [range] by taking
     * - the minimum of the [range] if [this] is smaller than every element in the [range],
     * - [this] if it is within the range, and
     * - the maximum of the [range] if [this] is larger than every element in the [range].
     */
    private fun Int.projectOnto(range: IntRange) = max(range.first, min(range.last, this))

    /**
     * @see [projectOnto]
     */
    private fun Float.projectOnto(range: IntRange) = max(range.first.toFloat(), min(range.last.toFloat(), this))

    private fun Double.format(digits: Int = 3) = String.format("%.${digits}f", this)
}

package nl.hannahsten.texifyidea.gutter

import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerProvider
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder
import com.intellij.ide.util.gotoByName.GotoFileCellRenderer
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.RequiredFileArgument
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexRequiredParamContent
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.Log
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.requiredParameters
import javax.swing.Icon

/**
 * @author Hannah Schellekens
 */
class LatexNavigationGutter : RelatedItemLineMarkerProvider() {

    override fun collectNavigationMarkers(
        element: PsiElement,
        result: MutableCollection<in RelatedItemLineMarkerInfo<*>>
    ) {
        // Gutters should only be used with leaf elements.
        // We assume gutter icons only have to be shown for elements in required parameters
        if (element.firstChild != null || element.parentOfType(LatexRequiredParamContent::class) == null) return

        // Only make markers when dealing with commands.
        val command = element.parentOfType(LatexCommands::class) ?: return

        val fullCommand = command.name ?: return

        // Fetch the corresponding LatexRegularCommand object.
        val commandHuh = LatexCommand.lookup(fullCommand.substring(1)) ?: return

        val arguments = commandHuh.first().getArgumentsOf(RequiredFileArgument::class.java)
        if (arguments.isEmpty()) {
            return
        }

        // Actual required parameters from the PSI tree
        val requiredParams = command.requiredParameters()
        if (requiredParams.isEmpty()) {
            return
        }

        val referencesList = command.references.filterIsInstance<InputFileReference>()
        if (referencesList.isEmpty()) return

        val files = referencesList.mapNotNull { it.resolve() }
        val gutterFile = files.firstOrNull()?.virtualFile
        val extension = gutterFile?.let {
            if (it.name.endsWith("synctex.gz")) "synctex.gz" else it.extension
        }
        // Gutter requires a smaller icon per IJ SDK docs.
        val icon = TexifyIcons.getIconFromExtension(extension, smaller = true)

        try {
            val builder = NavigationGutterIconBuilder
                .create(icon)
                .setTargets(files)
                .setPopupTitle("Navigate to Referenced File")
                .setTooltipText("Go to referenced file")
                .setCellRenderer { GotoFileCellRenderer(0) }

            result.add(builder.createLineMarkerInfo(element))
        }
        catch (e: NoSuchMethodError) {
            // I have no idea what could lead to the setCellRenderer method not existing, as it almost always exists, but in April 2022 there are suddenly 10 reports in which it doesn't.
            Log.warn(e.message ?: "NoSuchMethodError in LatexNavigationGutter")
        }
    }

    override fun getName(): String {
        return "Navigate to referenced file"
    }

    override fun getIcon(): Icon {
        return TexifyIcons.LATEX_FILE_SMALLER
    }
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.Annotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.BibtexBracedString
import nl.hannahsten.texifyidea.psi.BibtexBracedVerbatim
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexKey
import nl.hannahsten.texifyidea.psi.BibtexQuotedString
import nl.hannahsten.texifyidea.psi.BibtexQuotedVerbatim
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.tokenType
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class BibtexAnnotator : Annotator {

    override fun annotate(element: PsiElement, holder: AnnotationHolder) {
        when (element) {
            is BibtexBracedString -> annotate(element, holder)
            is BibtexBracedVerbatim -> annotate(element, holder)
            is BibtexQuotedString -> annotate(element, holder)
            is BibtexQuotedVerbatim -> annotate(element, holder)
            is BibtexKey -> annotate(element, holder)
        }
    }

    /**
     * Adds syntax highlighting to all {Braced Strings}.
     */
    private fun annotate(bracedString: BibtexBracedString, holder: AnnotationHolder) {
        holder.newAnnotation(HighlightSeverity.INFORMATION, "")
            .range(bracedString)
            .textAttributes(BibtexSyntaxHighlighter.VALUE)
            .create()
    }

    /**
     * Adds syntax highlighting to all {Braced Verbatim Strings}.
     */
    private fun annotate(bracedVerbatim: BibtexBracedVerbatim, holder: AnnotationHolder) {
        holder.newAnnotation(HighlightSeverity.INFORMATION, "")
            .range(bracedVerbatim)
            .textAttributes(BibtexSyntaxHighlighter.VALUE)
            .create()
    }

    /**
     * Adds syntax highlighting to all "Quoted Strings".
     */
    private fun annotate(quotedString: BibtexQuotedString, holder: AnnotationHolder) {
        holder.newAnnotation(HighlightSeverity.INFORMATION, "")
            .range(quotedString)
            .textAttributes(BibtexSyntaxHighlighter.STRING)
            .create()
    }

    /**
     * Adds syntax highlighting to all "Quoted Verbatim Strings".
     */
    private fun annotate(quotedVerbatim: BibtexQuotedVerbatim, holder: AnnotationHolder) {
        holder.newAnnotation(HighlightSeverity.INFORMATION, "")
            .range(quotedVerbatim)
            .textAttributes(BibtexSyntaxHighlighter.STRING)
            .create()
    }

    /**
     * Adds syntax highlighting to all Keys.
     */
    private fun annotate(key: BibtexKey, holder: AnnotationHolder) {
        val entry = key.parentOfType(BibtexEntry::class) ?: return
        val token = entry.tokenType().lowercase(Locale.getDefault())
        if (token == "@string") {
            return
        }

        holder.newAnnotation(HighlightSeverity.INFORMATION, "")
            .range(key)
            .textAttributes(BibtexSyntaxHighlighter.KEY)
            .create()
    }
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.openapi.options.colors.AttributesDescriptor
import com.intellij.openapi.options.colors.ColorDescriptor
import com.intellij.openapi.options.colors.ColorSettingsPage
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens
 */
open class BibtexColorSettingsPage : ColorSettingsPage {

    companion object {

        private val descriptors = arrayOf(
            AttributesDescriptor("Assignment", BibtexSyntaxHighlighter.ASSIGNMENT),
            AttributesDescriptor("Braces", BibtexSyntaxHighlighter.BRACES),
            AttributesDescriptor("Comments", BibtexSyntaxHighlighter.COMMENTS),
            AttributesDescriptor("Concatenation", BibtexSyntaxHighlighter.CONCATENATION),
            AttributesDescriptor("Identifier", BibtexSyntaxHighlighter.IDENTIFIER),
            AttributesDescriptor("Key", BibtexSyntaxHighlighter.KEY),
            AttributesDescriptor("Number", BibtexSyntaxHighlighter.NUMBER),
            AttributesDescriptor("String", BibtexSyntaxHighlighter.STRING),
            AttributesDescriptor("Type token", BibtexSyntaxHighlighter.TYPE_TOKEN),
            AttributesDescriptor("Value", BibtexSyntaxHighlighter.VALUE)
        )

        private val demoTags = mapOf(
            "key" to BibtexSyntaxHighlighter.KEY,
            "string" to BibtexSyntaxHighlighter.STRING,
            "value" to BibtexSyntaxHighlighter.VALUE
        )
    }

    override fun getIcon() = TexifyIcons.BIBLIOGRAPHY_FILE

    override fun getHighlighter() = BibtexSyntaxHighlighter()

    override fun getDemoText() =
        """
                |%
                |%  Comments are amazing
                |%
                |
                |@Preamble{<string>"Fancy
                |    \newcommand{\peanutbutter}{Jelly Time!}
                |"</string>}
                |
                |@string{ <key>test</key> = <string>"String definition"</string> }
                |@string{ <key>identifier</key> = <string>"Identifier"</string> }
                |
                |@tokentype{
                |    <key>key</key> = <string>"String"</string> # <value>{Braced string}</value> # identifier
                |},
                |
                |@article{small,
                |    <key>author</key> = <value>{Henry}</value> # test,
                |    <key>title</key> = <value>{A small paper}</value>,
                |    <key>journal</key> = <string>"The journal of smiles"</string>,
                |    <key>year</key> = 1997,
                |    <key>volume</key> = -1,
                |    <key>note</key> = <value>{to appear}</value>,
                |}
        """.trimMargin()

    override fun getAdditionalHighlightingTagToDescriptorMap() = demoTags

    override fun getAttributeDescriptors() = descriptors

    override fun getColorDescriptors(): Array<out ColorDescriptor> = ColorDescriptor.EMPTY_ARRAY!!

    override fun getDisplayName() = "BibTeX"
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.lang.BracePair
import com.intellij.lang.PairedBraceMatcher
import com.intellij.psi.PsiFile
import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.psi.BibtexTypes

/**
 * @author Hannah Schellekens
 */
open class BibtexPairedBraceMatcher : PairedBraceMatcher {

    companion object {

        private val bracePairs = arrayOf(
            BracePair(BibtexTypes.OPEN_BRACE, BibtexTypes.CLOSE_BRACE, false),
            BracePair(BibtexTypes.OPEN_PARENTHESIS, BibtexTypes.CLOSE_PARENTHESIS, false),
            BracePair(BibtexTypes.QUOTES, BibtexTypes.END_QUOTES, false)
        )
    }

    override fun getPairs() = bracePairs

    override fun isPairedBracesAllowedBeforeType(leftBrace: IElementType, contextType: IElementType?): Boolean {
        return true
    }

    override fun getCodeConstructStart(file: PsiFile?, openingBraceOffset: Int) = openingBraceOffset
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors.*
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey
import com.intellij.openapi.fileTypes.SyntaxHighlighterBase
import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.grammar.BibtexLexerAdapter
import nl.hannahsten.texifyidea.psi.BibtexTypes

/**
 * @author Hannah Schellekens
 */
open class BibtexSyntaxHighlighter : SyntaxHighlighterBase() {

    companion object {

        val ASSIGNMENT = createTextAttributesKey("BIBTEX_ASSIGNMENT", OPERATION_SIGN)
        val BRACES = createTextAttributesKey("BIBTEX_BRACES", LatexSyntaxHighlighter.BRACES)
        val COMMENTS = createTextAttributesKey("BIBTEX_COMMENTS", LatexSyntaxHighlighter.COMMENT)
        val CONCATENATION = createTextAttributesKey("BIBTEX_CONCATENATION", OPERATION_SIGN)
        val IDENTIFIER = createTextAttributesKey("BIBTEX_IDENTIFIER", INSTANCE_FIELD)
        val KEY = createTextAttributesKey("BIBTEX_KEY", PARAMETER)
        val NUMBER = createTextAttributesKey("BIBTEX_NUMBER", DefaultLanguageHighlighterColors.NUMBER)
        val STRING = createTextAttributesKey("BIBTEX_STRING", DefaultLanguageHighlighterColors.STRING)
        val TYPE_TOKEN = createTextAttributesKey("BIBTEX_TYPE_TOKEN", KEYWORD)
        val VALUE = createTextAttributesKey("BIBTEX_VALUE", DefaultLanguageHighlighterColors.IDENTIFIER)

        val ASSIGNMENT_KEYS = arrayOf(ASSIGNMENT)
        val BRACES_KEYS = arrayOf(BRACES)
        val COMMENT_KEYS = arrayOf(COMMENTS)
        val CONCATENATION_KEYS = arrayOf(CONCATENATION)
        val IDENTIFIER_KEYS = arrayOf(IDENTIFIER)
        val KEY_KEYS = arrayOf(KEY)
        val NUMBER_KEYS = arrayOf(NUMBER)
        val STRING_KEYS = arrayOf(STRING)
        val TYPE_TOKEN_KEYS = arrayOf(TYPE_TOKEN)
        val VALUE_KEYS = arrayOf(VALUE)
        val EMPTY_KEYS = emptyArray<TextAttributesKey>()
    }

    override fun getHighlightingLexer() = BibtexLexerAdapter()

    override fun getTokenHighlights(tokenType: IElementType?) = when (tokenType) {
        BibtexTypes.ASSIGNMENT -> ASSIGNMENT_KEYS
        BibtexTypes.OPEN_BRACE, BibtexTypes.CLOSE_BRACE, BibtexTypes.OPEN_PARENTHESIS -> BRACES_KEYS
        BibtexTypes.COMMENT, BibtexTypes.COMMENT_TOKEN -> COMMENT_KEYS
        BibtexTypes.CONCATENATE -> CONCATENATION_KEYS
        BibtexTypes.IDENTIFIER, BibtexTypes.VERBATIM_IDENTIFIER -> IDENTIFIER_KEYS
        BibtexTypes.KEY -> KEY_KEYS
        BibtexTypes.NUMBER -> NUMBER_KEYS
        BibtexTypes.TYPE_TOKEN -> TYPE_TOKEN_KEYS
        BibtexTypes.STRING, BibtexTypes.QUOTED_STRING, BibtexTypes.QUOTED_VERBATIM -> STRING_KEYS
        BibtexTypes.CONTENT, BibtexTypes.BRACED_STRING, BibtexTypes.BRACED_VERBATIM -> VALUE_KEYS
        else -> EMPTY_KEYS
    }
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile

/**
 * @author Hannah Schellekens
 */
open class BibtexSyntaxHighlighterFactory : SyntaxHighlighterFactory() {

    override fun getSyntaxHighlighter(project: Project?, file: VirtualFile?) = BibtexSyntaxHighlighter()
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.Annotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.lang.commands.LatexGenericMathCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.*
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.parser.*

/**
 * Provide syntax highlighting for composite elements.
 *
 * @author Hannah Schellekens
 */
open class LatexAnnotator : Annotator {

    companion object {
        /**
         * All user defined commands, cached because it requires going over all commands, which we don't want to do for every command we need to annotate.
         */
        var allUserDefinedCommands = emptyList<String>()
    }

    override fun annotate(psiElement: PsiElement, annotationHolder: AnnotationHolder) {
        // Math display
        if (psiElement is LatexInlineMath) {
            annotateInlineMath(psiElement, annotationHolder)
        }
        else if (psiElement is LatexDisplayMath ||
            (psiElement is LatexEnvironment && psiElement.isContext(Environment.Context.MATH))
        ) {
            annotateDisplayMath(psiElement, annotationHolder)

            // Begin/End commands
            if (psiElement is LatexEnvironment) {
                annotationHolder.newAnnotation(HighlightSeverity.INFORMATION, "")
                    .range(TextRange.from(psiElement.beginCommand.textOffset, 6))
                    .textAttributes(LatexSyntaxHighlighter.COMMAND_MATH_DISPLAY)
                    .create()

                annotationHolder.newAnnotation(HighlightSeverity.INFORMATION, "")
                    .range(TextRange.from(psiElement.endCommand?.textOffset ?: psiElement.textOffset, 4))
                    .textAttributes(LatexSyntaxHighlighter.COMMAND_MATH_DISPLAY)
                    .create()
            }
        }
        // Key value pairs. Match on the common interface so we catch LatexKeyValPair and LatexStrictKeyValPair.
        else if (psiElement is LatexOptionalKeyValPair) {
            annotateKeyValuePair(psiElement, annotationHolder)
        }
        // Optional parameters.
        else if (psiElement is LatexOptionalParam) {
            annotateOptionalParameters(psiElement, annotationHolder)
        }
        // Commands.
        else if (psiElement is LatexCommands) {
            annotateCommands(psiElement, annotationHolder)
        }
        else if (psiElement.isComment()) {
            annotationHolder.newSilentAnnotation(HighlightSeverity.INFORMATION)
                .range(psiElement.textRange)
                .textAttributes(LatexSyntaxHighlighter.COMMENT)
                .create()
        }
    }

    /**
     * Annotates an inline math element and its children.
     *
     * All elements will be coloured accoding to [LatexSyntaxHighlighter.INLINE_MATH] and
     * all commands that are contained in the math environment get styled with
     * [LatexSyntaxHighlighter.COMMAND_MATH_INLINE].
     */
    private fun annotateInlineMath(
        inlineMathElement: LatexInlineMath,
        annotationHolder: AnnotationHolder
    ) {
        annotationHolder.newSilentAnnotation(HighlightSeverity.INFORMATION)
            .range(inlineMathElement)
            .textAttributes(LatexSyntaxHighlighter.INLINE_MATH)
            .create()

        annotateMathCommands(
            inlineMathElement.childrenOfType(LatexCommands::class), annotationHolder,
            LatexSyntaxHighlighter.COMMAND_MATH_INLINE
        )
    }

    /**
     * Annotates a display math element and its children.
     *
     * All elements will be coloured accoding to [LatexSyntaxHighlighter.DISPLAY_MATH] and
     * all commands that are contained in the math environment get styled with
     * [LatexSyntaxHighlighter.COMMAND_MATH_DISPLAY].
     */
    private fun annotateDisplayMath(
        displayMathElement: PsiElement,
        annotationHolder: AnnotationHolder
    ) {
        annotationHolder.newSilentAnnotation(HighlightSeverity.INFORMATION)
            .range(displayMathElement)
            .textAttributes(LatexSyntaxHighlighter.DISPLAY_MATH)
            .create()

        annotateMathCommands(
            displayMathElement.childrenOfType(LatexCommands::class), annotationHolder,
            LatexSyntaxHighlighter.COMMAND_MATH_DISPLAY
        )
    }

    /**
     * Annotates all command tokens of the commands that are included in the `elements`.
     *
     * @param elements
     *              All elements to handle. Only elements that are [LatexCommands] are considered.
     * @param highlighter
     *              The attributes to apply to all command tokens.
     */
    private fun annotateMathCommands(
        elements: Collection<PsiElement>,
        annotationHolder: AnnotationHolder,
        highlighter: TextAttributesKey
    ) {
        for (element in elements) {
            if (element !is LatexCommands) {
                continue
            }

            val token = element.commandToken

            annotationHolder.newSilentAnnotation(HighlightSeverity.INFORMATION)
                .range(token)
                .textAttributes(highlighter)
                .create()

            if (element.name == TEXT.cmd || element.name == INTERTEXT.name) {
                // Avoid creating an Annotation without calling the create() method
                val range = element.requiredParameters().firstOrNull() ?: continue
                annotationHolder.newSilentAnnotation(HighlightSeverity.INFORMATION)
                    .range(range)
                    .textAttributes(LatexSyntaxHighlighter.MATH_NESTED_TEXT)
                    .create()
            }
        }
    }

    private fun annotateKeyValuePair(element: LatexOptionalKeyValPair, annotationHolder: AnnotationHolder) {
        val value = element.keyValValue ?: return

        annotationHolder.newSilentAnnotation(HighlightSeverity.INFORMATION)
            .range(TextRange(element.optionalKeyValKey.endOffset, value.startOffset))
            .textAttributes(LatexSyntaxHighlighter.SEPARATOR_EQUALS)
            .create()
    }

    /**
     * Annotates the given optional parameters of commands.
     */
    private fun annotateOptionalParameters(
        optionalParamElement: LatexOptionalParam,
        annotationHolder: AnnotationHolder
    ) {
        for (
        element in optionalParamElement.optionalKeyValPairList
        ) {
            if (element !is LatexOptionalParamContent) {
                continue
            }

            val toStyle = element.parameterText ?: continue

            annotationHolder.newSilentAnnotation(HighlightSeverity.INFORMATION)
                .range(toStyle)
                .textAttributes(LatexSyntaxHighlighter.OPTIONAL_PARAM)
                .create()
        }
    }

    /**
     * Annotates the given required parameters of commands.
     */
    private fun annotateCommands(command: LatexCommands, annotationHolder: AnnotationHolder) {
        annotateStyle(command, annotationHolder)

        // Make user-defined commands highlighting customizable
        if (allUserDefinedCommands.isEmpty()) {
            allUserDefinedCommands = LatexDefinitionIndex.getItems(command.project)
                .filter { it.isCommandDefinition() }
                .mapNotNull { it.definedCommandName() }
        }
        if (command.name in allUserDefinedCommands) {
            annotationHolder.newSilentAnnotation(HighlightSeverity.INFORMATION)
                .textAttributes(LatexSyntaxHighlighter.USER_DEFINED_COMMAND_KEY)
                .range(command.commandToken)
                .create()
        }

        // Label references.
        val style = when (command.name) {
            in CommandMagic.labelReferenceWithoutCustomCommands -> {
                LatexSyntaxHighlighter.LABEL_REFERENCE
            }
            // Label definitions.
            in getLabelDefinitionCommands() -> {
                LatexSyntaxHighlighter.LABEL_DEFINITION
            }
            // Bibliography references (citations).
            in CommandMagic.bibliographyReference -> {
                LatexSyntaxHighlighter.BIBLIOGRAPHY_REFERENCE
            }
            // Label definitions.
            in CommandMagic.bibliographyItems -> {
                LatexSyntaxHighlighter.BIBLIOGRAPHY_DEFINITION
            }

            else -> return
        }

        command.requiredParameters().firstOrNull()?.let {
            annotationHolder.annotateRequiredParameter(it, style)
        }
    }

    /**
     * Annotates the command according to its font style, i.e. \textbf{} gets annotated with the `STYLE_BOLD` style.
     */
    private fun annotateStyle(command: LatexCommands, annotationHolder: AnnotationHolder) {
        val style = when (command.name) {
            TEXTBF.cmd -> LatexSyntaxHighlighter.STYLE_BOLD
            TEXTIT.cmd -> LatexSyntaxHighlighter.STYLE_ITALIC
            LatexGenericRegularCommand.UNDERLINE.cmd -> LatexSyntaxHighlighter.STYLE_UNDERLINE
            SOUT.cmd -> LatexSyntaxHighlighter.STYLE_STRIKETHROUGH
            TEXTSC.cmd -> LatexSyntaxHighlighter.STYLE_SMALL_CAPITALS
            OVERLINE.cmd -> LatexSyntaxHighlighter.STYLE_OVERLINE
            TEXTTT.cmd -> LatexSyntaxHighlighter.STYLE_TYPEWRITER
            TEXTSL.cmd -> LatexSyntaxHighlighter.STYLE_SLANTED
            else -> return
        }

        command.requiredParameters().firstOrNull()?.let {
            annotationHolder.annotateRequiredParameter(it, style)
        }
    }

    /**
     * Annotates the contents of the given parameter with the given style.
     */
    @Suppress("USELESS_CAST")
    private fun AnnotationHolder.annotateRequiredParameter(parameter: LatexRequiredParam, style: TextAttributesKey) {
        val firstContentChild = parameter.firstChildOfType(LatexContent::class)
        val firstParamChild = parameter.firstChildOfType(LatexRequiredParamContent::class)

        if (firstContentChild != null) {
            this.newSilentAnnotation(HighlightSeverity.INFORMATION)
                .range(firstContentChild)
                .textAttributes(style)
                .create()
        }
        else if (firstParamChild != null) {
            parameter.childrenOfType(LeafPsiElement::class)
                .filter {
                    it.elementType == LatexTypes.NORMAL_TEXT_WORD
                }
                .map {
                    it as PsiElement
                }
                .forEach {
                    this.newSilentAnnotation(HighlightSeverity.INFORMATION)
                        .range(it)
                        .textAttributes(style)
                        .create()
                }
        }
    }
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.openapi.options.colors.AttributesDescriptor
import com.intellij.openapi.options.colors.ColorDescriptor
import com.intellij.openapi.options.colors.ColorSettingsPage
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexColorSettingsPage : ColorSettingsPage {

    companion object {

        /*
         * You can group multiple descriptors by prefixing it with the group name and '//'. FYI.
         */
        @JvmStatic
        val DESCRIPTORS = arrayOf(
            AttributesDescriptor("Braces", LatexSyntaxHighlighter.BRACES),
            AttributesDescriptor("Brackets", LatexSyntaxHighlighter.BRACKETS),
            AttributesDescriptor("Commands//Optional parameters", LatexSyntaxHighlighter.OPTIONAL_PARAM),
            AttributesDescriptor("Commands//Equals sign in key-value pairs", LatexSyntaxHighlighter.SEPARATOR_EQUALS),
            AttributesDescriptor("Commands//Commands", LatexSyntaxHighlighter.COMMAND),
            AttributesDescriptor("Commands//User defined commands", LatexSyntaxHighlighter.USER_DEFINED_COMMAND),
            AttributesDescriptor("Commands//Commands in inline math mode", LatexSyntaxHighlighter.COMMAND_MATH_INLINE),
            AttributesDescriptor("Commands//Commands in display math mode", LatexSyntaxHighlighter.COMMAND_MATH_DISPLAY),
            AttributesDescriptor("Commands//Stars", LatexSyntaxHighlighter.STAR),
            AttributesDescriptor("Comments//Regular comment", LatexSyntaxHighlighter.COMMENT),
            AttributesDescriptor("Comments//Magic comment", LatexSyntaxHighlighter.MAGIC_COMMENT),
            AttributesDescriptor("References//Label definition", LatexSyntaxHighlighter.LABEL_DEFINITION),
            AttributesDescriptor("References//Label reference", LatexSyntaxHighlighter.LABEL_REFERENCE),
            AttributesDescriptor("References//Bibliography item", LatexSyntaxHighlighter.BIBLIOGRAPHY_DEFINITION),
            AttributesDescriptor("References//Citation", LatexSyntaxHighlighter.BIBLIOGRAPHY_REFERENCE),
            AttributesDescriptor("Math//Inline math", LatexSyntaxHighlighter.INLINE_MATH),
            AttributesDescriptor("Math//Display math", LatexSyntaxHighlighter.DISPLAY_MATH),
            AttributesDescriptor("Math//Text in math", LatexSyntaxHighlighter.MATH_NESTED_TEXT),

            // Styles
            AttributesDescriptor("Font style//Bold", LatexSyntaxHighlighter.STYLE_BOLD),
            AttributesDescriptor("Font style//Italics", LatexSyntaxHighlighter.STYLE_ITALIC),
            AttributesDescriptor("Font style//Underline", LatexSyntaxHighlighter.STYLE_UNDERLINE),
            AttributesDescriptor("Font style//Strikethrough", LatexSyntaxHighlighter.STYLE_STRIKETHROUGH),
            AttributesDescriptor("Font style//Small capitals", LatexSyntaxHighlighter.STYLE_SMALL_CAPITALS),
            AttributesDescriptor("Font style//Overline", LatexSyntaxHighlighter.STYLE_OVERLINE),
            AttributesDescriptor("Font style//Typewriter", LatexSyntaxHighlighter.STYLE_TYPEWRITER),
            AttributesDescriptor("Font style//Slanted", LatexSyntaxHighlighter.STYLE_SLANTED)
        )

        @JvmStatic
        val DEMO_TAGS = mapOf(
            "displayCommand" to LatexSyntaxHighlighter.COMMAND_MATH_DISPLAY,
            "inlineCommand" to LatexSyntaxHighlighter.COMMAND_MATH_INLINE,
            "userDefinedCommand" to LatexSyntaxHighlighter.USER_DEFINED_COMMAND,
            "displayMath" to LatexSyntaxHighlighter.DISPLAY_MATH,
            "inlineMath" to LatexSyntaxHighlighter.INLINE_MATH,
            "textInMath" to LatexSyntaxHighlighter.MATH_NESTED_TEXT,
            "optionalParam" to LatexSyntaxHighlighter.OPTIONAL_PARAM,
            "comment" to LatexSyntaxHighlighter.COMMENT,
            "magicComment" to LatexSyntaxHighlighter.MAGIC_COMMENT,
            "equalsSeparator" to LatexSyntaxHighlighter.SEPARATOR_EQUALS,
            "labelDefinition" to LatexSyntaxHighlighter.LABEL_DEFINITION,
            "reference" to LatexSyntaxHighlighter.LABEL_REFERENCE,
            "bibliographyDefinition" to LatexSyntaxHighlighter.BIBLIOGRAPHY_DEFINITION,
            "bibliographyReference" to LatexSyntaxHighlighter.BIBLIOGRAPHY_REFERENCE,
            "styleBold" to LatexSyntaxHighlighter.STYLE_BOLD,
            "styleItalic" to LatexSyntaxHighlighter.STYLE_ITALIC,
            "styleUnderline" to LatexSyntaxHighlighter.STYLE_UNDERLINE,
            "styleStrikethrough" to LatexSyntaxHighlighter.STYLE_STRIKETHROUGH,
            "styleSmallCapitals" to LatexSyntaxHighlighter.STYLE_SMALL_CAPITALS,
            "styleOverline" to LatexSyntaxHighlighter.STYLE_OVERLINE,
            "styleTypewriter" to LatexSyntaxHighlighter.STYLE_TYPEWRITER,
            "styleSlanted" to LatexSyntaxHighlighter.STYLE_SLANTED
        )
    }

    override fun getIcon() = TexifyIcons.LATEX_FILE

    override fun getHighlighter() = LatexSyntaxHighlighter()

    override fun getDemoText() =
        """
                |<magicComment>%! Compiler = pdfLaTeX</magicComment>
                |%  An amazing example for LaTeX.
                |\documentclass[<optionalParam>12pt,a4paper</optionalParam>]{article}
                |
                |% Package imports.
                |\usepackage{amsmath}
                |\usepackage{comment}
                |\usepackage{listings}
                |
                |\lstset{language<equalsSeparator>=</equalsSeparator>Kotlin}
                |
                |\newcommand{\betterphi}{\varphi}
                |\newcommand{\myphi}{<userDefinedCommand>\betterphi</userDefinedCommand>}
                |
                |% Start document.
                |<magicComment>%! Suppress = NonBreakingSpace</magicComment>
                |\begin{document}
                |
                |    % Make title.
                |    \title{A Very Simple \LaTeXe{} Template}
                |    \author{
                |            Henk-Jan\\Department of YUROP\\University of Cheese\\
                |            Windmill City, 2198 AL, \underline{<styleUnderline>Tulipa</styleUnderline>}
                |    }
                |    \date{\today}
                |    \maketitle
                |
                |    % Start writing amazing stuff now.
                |    \begin{abstract}
                |        This is the paper's abstract \ldots.
                |    \end{abstract}
                |
                |    <comment>\begin{comment}
                |        Yes, even comment environments get highlighted.
                |    \end{comment}</comment>
                |
                |    \section{Introduction}\label{<labelDefinition>sec:introduction</labelDefinition>}
                |    This is time for all good men to come to the aid of
                |    their party!
                |    For the end see~\ref{<reference>sec:conclusions</reference>}.
                |
                |    \begin{lstlisting}[label<equalsSeparator>=</equalsSeparator>test]
                |       println("Hello world!")
                |    \end{lstlisting}
                |
                |    \paragraph*{Mathematics}
                |    Please take a look at the value of <inlineMath>${'$'}x <inlineCommand>\times</inlineCommand>
                |    <inlineCommand>\frac</inlineCommand>{5}{<inlineCommand>\sqrt</inlineCommand>{3}}${'$'}</inlineMath> in the following equation:
                |    <displayMath>\[
                |       x <displayCommand>\times</displayCommand> <displayCommand>\frac</displayCommand>{5}{<displayCommand>\sqrt</displayCommand>{3}} = y <displayCommand>\cdot</displayCommand> <displayCommand>\max\left</displayCommand>{ 4, <displayCommand>\alpha</displayCommand>, 6 <displayCommand>\right</displayCommand>} +
                |           <displayCommand>\sqrt</displayCommand>[<optionalParam>1234</optionalParam>]{5678}.
                |    \]</displayMath>
                |
                |    \section{More work}\label{<labelDefinition>sec:moreWork</labelDefinition>}
                |    A much longer \LaTeXe{} example was written by Henk-Jan~\cite{<bibliographyReference>Gil:02</bibliographyReference>}. But
                |    we can also just do some more epic plugin showoffy stuff like
                |    <displayMath>\begin{align}
                |       <displayCommand>\text</displayCommand>{Stuff here is also highlighted, and also }
                |       <displayCommand>\sum</displayCommand>_{i=0}^n <displayCommand>\left</displayCommand>( i <displayCommand>\right</displayCommand>)
                |    \begin{align}</displayMath>
                |
                |    \section{Results}\label{<labelDefinition>sec:results</labelDefinition>}
                |    In this section we describe the results. So basically <inlineMath>${'$'}x${'$'}</inlineMath> but maybe
                |    also <inlineMath>${'$'}<inlineCommand>\hat</inlineCommand>{x}^{2y}<inlineCommand>\text</inlineCommand><textInMath>{if <inlineMath>${'$'}x > 0${'$'}</inlineMath> }</textInMath>${'$'}</inlineMath>.
                |
                |    Also, some text styles:
                |    \textbf{<styleBold>Bold</styleBold>}
                |    \textit{<styleItalic>Italic</styleItalic>}
                |    \overline{<styleOverline>Overline</styleOverline>}
                |    \textsl{<styleSlanted>Slanted</styleSlanted>}
                |    \textsc{<styleSmallCapitals>SMALL CAPITALS</styleSmallCapitals>}
                |    \sout{<styleStrikethrough>Strikethrough</styleStrikethrough>}
                |    \texttt{<styleTypewriter>Typewriter</styleTypewriter>}
                |    \underline{<styleUnderline>Underline</styleUnderline>}
                |
                |    \section{Conclusions}\label{<labelDefinition>sec:conclusions</labelDefinition>}
                |    We worked hard, and achieved very little. Or did we?
                |
                |    % Another extremely descriptive comment.
                |    \bibliographystyle{abbrv}
                |    \bibliography{main}
                |
                |    \begin{thebibliography}{9}
                |        \bibitem{<bibliographyDefinition>latexcompanion</bibliographyDefinition>}
                |        Michel Goossens, Frank Mittelbach, and Alexander Samarin.
                |        \textit{<styleItalic>The \LaTeX\ Companion</styleItalic>}.
                |        Addison-Wesley, Reading, Massachusetts, 1993.
                |    \end{thebibliography}
                |
                |\end{document}
        """.trimMargin()

    override fun getAdditionalHighlightingTagToDescriptorMap() = DEMO_TAGS

    override fun getAttributeDescriptors() = DESCRIPTORS

    override fun getColorDescriptors(): Array<out ColorDescriptor> = ColorDescriptor.EMPTY_ARRAY!!

    override fun getDisplayName() = "LaTeX"
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.lang.BracePair
import com.intellij.lang.PairedBraceMatcher
import com.intellij.psi.PsiFile
import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.psi.LatexTypes

/**
 * @author Sten Wessel
 */
class LatexPairedBraceMatcher : PairedBraceMatcher {

    companion object {

        private val bracePairs = arrayOf(
            BracePair(LatexTypes.DISPLAY_MATH_START, LatexTypes.DISPLAY_MATH_END, true),
            BracePair(LatexTypes.INLINE_MATH_START, LatexTypes.INLINE_MATH_END, true),
            BracePair(LatexTypes.BEGIN_TOKEN, LatexTypes.END_TOKEN, false),
            BracePair(LatexTypes.OPEN_PAREN, LatexTypes.CLOSE_PAREN, false),
            BracePair(LatexTypes.OPEN_BRACE, LatexTypes.CLOSE_BRACE, false),
            BracePair(LatexTypes.OPEN_BRACKET, LatexTypes.CLOSE_BRACKET, false),
            BracePair(LatexTypes.BEGIN_PSEUDOCODE_BLOCK, LatexTypes.END_PSEUDOCODE_BLOCK, false)
        )
    }

    override fun getPairs() = bracePairs

    override fun isPairedBracesAllowedBeforeType(lbraceType: IElementType, contextType: IElementType?): Boolean {
        // Automatic completion by IJ fails with multiple characters for rbrace.
        return lbraceType !== LatexTypes.DISPLAY_MATH_START
    }

    override fun getCodeConstructStart(file: PsiFile, openingBraceOffset: Int) = openingBraceOffset
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.lexer.Lexer
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.fileTypes.SyntaxHighlighterBase
import com.intellij.psi.tree.IElementType
import com.intellij.psi.tree.TokenSet
import nl.hannahsten.texifyidea.grammar.LatexLexerAdapter
import nl.hannahsten.texifyidea.psi.LatexTypes

/**
 * Syntax highlighting for lexer tokens (for composite elements, see [LatexAnnotator]).
 *
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexSyntaxHighlighter : SyntaxHighlighterBase() {

    override fun getHighlightingLexer(): Lexer {
        return LatexLexerAdapter()
    }

    override fun getTokenHighlights(tokenType: IElementType): Array<out TextAttributesKey?> {
        return if (tokenType == LatexTypes.OPEN_BRACE || tokenType == LatexTypes.CLOSE_BRACE) {
            BRACES_KEYS
        }
        else if (tokenType == LatexTypes.OPEN_BRACKET || tokenType == LatexTypes.CLOSE_BRACKET) {
            BRACKET_KEYS
        }
        else if (tokenType == LatexTypes.MAGIC_COMMENT_TOKEN) {
            MAGIC_COMMENT_KEYS
        }
        else if (tokenType == LatexTypes.COMMENT_TOKEN) {
            COMMENT_KEYS
        }
        else if (COMMAND_TOKENS.contains(tokenType)) {
            COMMAND_KEYS
        }
        else if (tokenType == LatexTypes.STAR) {
            STAR_KEYS
        }
        else {
            EMPTY_KEYS
        }
    }

    companion object {

        /*
         * TextAttributesKeys
         */
        val BRACES = createKey("LATEX_BRACES", DefaultLanguageHighlighterColors.BRACES)
        val BRACKETS = createKey("LATEX_BRACKETS", DefaultLanguageHighlighterColors.BRACKETS)
        val OPTIONAL_PARAM = createKey("LATEX_OPTIONAL_PARAM", DefaultLanguageHighlighterColors.PARAMETER)
        val COMMAND = createKey("LATEX_COMMAND", DefaultLanguageHighlighterColors.KEYWORD)
        val USER_DEFINED_COMMAND = createKey("LATEX_USER_DEFINED_COMMAND", DefaultLanguageHighlighterColors.KEYWORD)
        val COMMAND_MATH_INLINE = createKey("LATEX_COMMAND_MATH_INLINE", DefaultLanguageHighlighterColors.VALID_STRING_ESCAPE)
        val COMMAND_MATH_DISPLAY = createKey("LATEX_COMMAND_MATH_DISPLAY", DefaultLanguageHighlighterColors.VALID_STRING_ESCAPE)
        val COMMENT = createKey("LATEX_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT)
        val MAGIC_COMMENT = createKey("LATEX_MAGIC_COMMENT", DefaultLanguageHighlighterColors.DOC_COMMENT)
        val INLINE_MATH = createKey("LATEX_INLINE_MATH", DefaultLanguageHighlighterColors.STRING)
        val DISPLAY_MATH = createKey("LATEX_DISPLAY_MATH", DefaultLanguageHighlighterColors.STRING)
        val STAR = createKey("LATEX_STAR", DefaultLanguageHighlighterColors.DOT)
        val SEPARATOR_EQUALS = createKey("SEPARATOR_EQUALS", DefaultLanguageHighlighterColors.IDENTIFIER)
        val LABEL_DEFINITION = createKey("LATEX_LABEL_DEFINITION", DefaultLanguageHighlighterColors.IDENTIFIER)
        val LABEL_REFERENCE = createKey("LATEX_LABEL_REFERENCE", DefaultLanguageHighlighterColors.IDENTIFIER)
        val BIBLIOGRAPHY_DEFINITION = createKey("LATEX_BIBLIOGRAPHY_DEFINITION", LABEL_DEFINITION)
        val BIBLIOGRAPHY_REFERENCE = createKey("LATEX_BIBLIOGRAPHY_REFERENCE", LABEL_REFERENCE)
        val STYLE_BOLD = createKey("LATEX_STYLE_BOLD", DefaultLanguageHighlighterColors.IDENTIFIER)
        val STYLE_ITALIC = createKey("LATEX_STYLE_ITALIC", DefaultLanguageHighlighterColors.IDENTIFIER)
        val STYLE_UNDERLINE = createKey("LATEX_STYLE_UNDERLINE", DefaultLanguageHighlighterColors.IDENTIFIER)
        val STYLE_STRIKETHROUGH = createKey("LATEX_STYLE_STRIKETHROUGH", DefaultLanguageHighlighterColors.IDENTIFIER)
        val STYLE_SMALL_CAPITALS = createKey("LATEX_STYLE_SMALL_CAPITALS", DefaultLanguageHighlighterColors.IDENTIFIER)
        val STYLE_OVERLINE = createKey("LATEX_STYLE_OVERLINE", DefaultLanguageHighlighterColors.IDENTIFIER)
        val STYLE_TYPEWRITER = createKey("LATEX_STYLE_TYPEWRITER", DefaultLanguageHighlighterColors.IDENTIFIER)
        val STYLE_SLANTED = createKey("LATEX_STYLE_SLANTED", DefaultLanguageHighlighterColors.IDENTIFIER)
        val MATH_NESTED_TEXT = createKey("LATEX_MATH_NESTED_TEXT", DefaultLanguageHighlighterColors.IDENTIFIER)

        private val COMMAND_TOKENS = TokenSet.create(
            LatexTypes.COMMAND_TOKEN,
            LatexTypes.COMMAND_IFNEXTCHAR,
            LatexTypes.BEGIN_TOKEN,
            LatexTypes.END_TOKEN,
            LatexTypes.BEGIN_PSEUDOCODE_BLOCK,
            LatexTypes.MIDDLE_PSEUDOCODE_BLOCK,
            LatexTypes.END_PSEUDOCODE_BLOCK
        )

        /*
         * TextAttributeKey[]s
         */
        private val BRACES_KEYS = keys(BRACES)
        private val BRACKET_KEYS = keys(BRACKETS)
        private val COMMAND_KEYS = keys(COMMAND)
        val USER_DEFINED_COMMAND_KEY = USER_DEFINED_COMMAND
        private val COMMENT_KEYS = keys(COMMENT)
        private val MAGIC_COMMENT_KEYS = keys(MAGIC_COMMENT)
        private val INLINE_MATH_KEYS = keys(INLINE_MATH)
        private val DISPLAY_MATH_KEYS = keys(DISPLAY_MATH)
        private val STAR_KEYS = keys(STAR)
        private val EMPTY_KEYS = arrayOfNulls<TextAttributesKey>(0)
        private fun createKey(externalName: String, defaultStyle: TextAttributesKey): TextAttributesKey {
            return TextAttributesKey.createTextAttributesKey(externalName, defaultStyle)
        }

        private fun keys(vararg keys: TextAttributesKey): Array<out TextAttributesKey> {
            return keys
        }
    }
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile

/**
 * @author Sten Wessel
 */
class LatexSyntaxHighlighterFactory : SyntaxHighlighterFactory() {

    override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?) = LatexSyntaxHighlighter()
}

package nl.hannahsten.texifyidea.highlighting

import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.ExternalAnnotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import com.intellij.util.execution.ParametersListUtil
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import java.io.File

@JvmInline value class FileName(val value: String)

@JvmInline value class StartLine(val value: Int)

@JvmInline value class StartColumn(val value: Int)

@JvmInline value class EndLine(val value: Int)

@JvmInline value class EndColumn(val value: Int)

data class TextidoteAnnotatorInitialInfo(
    val fileName: String,
    val workingDirectory: File,
    val project: Project,
    val document: Document,
)

data class TextidoteWarning(
    val fileName: FileName,
    val startLine: StartLine,
    val startColumn: StartColumn,
    val endLine: EndLine,
    val endColumn: EndColumn,
    val message: String,
)

data class TextidoteAnnotationResult(
    val warnings: List<TextidoteWarning>,
    val document: Document,
)

class TextidoteAnnotator : DumbAware, ExternalAnnotator<TextidoteAnnotatorInitialInfo, TextidoteAnnotationResult>() {

    override fun collectInformation(file: PsiFile, editor: Editor, hasErrors: Boolean): TextidoteAnnotatorInitialInfo? {
        if (file.containingDirectory == null) return null

        return TextidoteAnnotatorInitialInfo(
            file.virtualFile.name,
            File(file.containingDirectory.virtualFile.path),
            file.project,
            editor.document,
        )
    }

    override fun doAnnotate(collectedInfo: TextidoteAnnotatorInitialInfo): TextidoteAnnotationResult {
        if (!TexifySettings.getInstance().enableTextidote) {
            return TextidoteAnnotationResult(emptyList(), collectedInfo.document)
        }

        val arguments = TexifySettings.getInstance().textidoteOptions
        val command = listOf("textidote") + ParametersListUtil.parse(arguments) + listOf(collectedInfo.fileName)
        val (output, exitCode) = runCommandWithExitCode(
            *command.toTypedArray(),
            workingDirectory = collectedInfo.workingDirectory,
            timeout = 10,
            returnExceptionMessage = true
        )

        // Since the user has explicitly enabled this inspection, we should raise an error if we cannot actually run textidote
        // todo add setting
        if (output == null) {
            Notification("LaTeX", "Could not run textidote", NotificationType.ERROR).notify(collectedInfo.project)
            return TextidoteAnnotationResult(emptyList(), collectedInfo.document)
        }

        // Example output:
        // main.tex(L8C16-L8C16): This sentence does not start with an uppercase letter.. Suggestions: [Blubblub] (39) "    \crefrange{blub}{blub}"
        val warnings =
            """(?<file>[^(\n]+)\(L(?<line1>\d+)C(?<column1>\d+)-L(?<line2>\d+)C(?<column2>\d+)\):\s*(?<message>[^"]+)"""".toRegex()
                .findAll(output)
                .filter { match -> match.groups["file"]?.value == collectedInfo.fileName }
                .mapNotNull { match ->
                    val line1 = match.groups["line1"]?.value?.toInt() ?: return@mapNotNull null
                    val column1 = match.groups["column1"]?.value?.toInt() ?: return@mapNotNull null
                    val line2 = match.groups["line2"]?.value?.toInt() ?: return@mapNotNull null
                    val column2 = match.groups["column2"]?.value?.toInt() ?: return@mapNotNull null
                    val message = match.groups["message"]?.value ?: return@mapNotNull null

                    TextidoteWarning(FileName(collectedInfo.fileName), StartLine(line1), StartColumn(column1), EndLine(line2), EndColumn(column2), message)
                }.toList()

        // Exit code is nonzero when warnings are found
        if (warnings.isEmpty() && exitCode != 0) {
            Notification(
                "LaTeX",
                "There was a problem getting Textidote results: $output",
                NotificationType.ERROR
            ).notify(collectedInfo.project)
        }

        return TextidoteAnnotationResult(warnings, collectedInfo.document)
    }

    override fun apply(file: PsiFile, annotationResult: TextidoteAnnotationResult, holder: AnnotationHolder) {
        for (warning in annotationResult.warnings) {
            val document = annotationResult.document

            // Don't show obsolete warnings out of range
            if (warning.endLine.value > document.lineCount) {
                continue
            }

            // In Textidote, everything is 1-based, and here everything is 0-based
            val lineStartOffset1 = document.getLineStartOffset(warning.startLine.value - 1)
            val lineStartOffset2 = document.getLineStartOffset(warning.endLine.value - 1)

            val startOffset = lineStartOffset1 + warning.startColumn.value - 1
            val endOffset = lineStartOffset2 + warning.endColumn.value - 1

            // Check if computed range falls in the document range.
            if (startOffset < document.textLength && endOffset < document.textLength) {
                holder.newAnnotation(HighlightSeverity.WARNING, warning.message + " (Textidote)")
                    .range(TextRange(startOffset, endOffset))
                    .create()
            }
        }

        super.apply(file, annotationResult, holder)
    }
}

package nl.hannahsten.texifyidea.index

import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.stubs.StringStubIndexExtension
import com.intellij.psi.stubs.StubIndex
import com.intellij.util.ArrayUtil
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.files.referencedFileSet

/**
 * @author Hannah Schellekens
 */
class BibtexEntryIndex : StringStubIndexExtension<BibtexEntry>() {

    /**
     * Get all the indexed [BibtexEntry]s in the project.
     */
    fun getIndexedEntries(project: Project) = getIndexedEntries(project, GlobalSearchScope.projectScope(project))

    /**
     * Get all the indexed [BibtexEntry]s in the given file.
     */
    fun getIndexedEntries(file: PsiFile) = getIndexedEntries(file.project, GlobalSearchScope.fileScope(file))

    /**
     * Get all the indexed [BibtexEntry]s in the complete file set of the given base file.
     *
     * @param baseFile
     *          The file whose file set must be analysed for indexed ids.
     */
    fun getIndexedEntriesInFileSet(baseFile: PsiFile): Collection<BibtexEntry> {
        val project = baseFile.project
        val searchFiles: MutableSet<VirtualFile> = baseFile.referencedFileSet()
            .asSequence()
            .mapNotNull(PsiFile::getVirtualFile)
            .toMutableSet()
        if (baseFile.virtualFile != null) {
            searchFiles.add(baseFile.virtualFile)
        }
        val scope = GlobalSearchScope.filesScope(project, searchFiles)
        return getIndexedEntries(project, scope)
    }

    /**
     * Get all the indexed [BibtexEntry]s in the given search scope.
     */
    private fun getIndexedEntries(project: Project, scope: GlobalSearchScope): Collection<BibtexEntry> {
        val commands: MutableCollection<BibtexEntry> = ArrayList()

        for (key in getKeys(project)) {
            commands.addAll(getEntryByName(key, project, scope))
        }

        return commands
    }

    fun getEntryByName(name: String, project: Project, scope: GlobalSearchScope): Collection<BibtexEntry> {
        return StubIndex.getElements(key, name, project, scope, BibtexEntry::class.java)
    }

    private fun getKeys(project: Project): Array<String> {
        if (DumbService.isDumb(project)) {
            return emptyArray()
        }

        val index = StubIndex.getInstance()
        val keys = index.getAllKeys(key, project)
        return ArrayUtil.toStringArray(keys)
    }

    override fun getKey() = BibtexEntryIndexKey.KEY
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StubIndexKey
import nl.hannahsten.texifyidea.psi.BibtexEntry

/**
 * See [BibtexEntryIndex]
 *
 * @author Felix Berlakovich
 */
object BibtexEntryIndexKey {

    @JvmField
    val KEY = StubIndexKey.createIndexKey<String, BibtexEntry>("nl.hannahsten.texifyidea.bibtex.entry")
}

package nl.hannahsten.texifyidea.index

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.stubs.StubIndexKey
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * @author Hannah Schellekens
 */
abstract class IndexCommandsUtilBase(
    indexKey: StubIndexKey<String, LatexCommands>
) : IndexUtilBase<LatexCommands>(LatexCommands::class.java, indexKey) {

    /**
     * Get all the commands that are in a given set of names (without slash).
     */
    fun getCommandsByNames(names: Set<String>, project: Project, scope: GlobalSearchScope): Collection<LatexCommands> {
        // Using cache creates a lot of test errors
        return getItems(project, scope, useCache = false).filter { it.name in names }
    }

    /**
     * Get all the commands that have a given name (without slash).
     */
    fun getCommandsByName(name: String, project: Project, scope: GlobalSearchScope): Collection<LatexCommands> {
        val nameSlash = if (name.startsWith('\\')) name else "\\$name"
        return getCommandsByNames(setOf(nameSlash), project, scope)
    }

    /**
     * Get all the commands with a certain name in a certain file. (with slash)
     */
    fun getCommandsByName(name: String, file: PsiFile): Collection<LatexCommands> {
        val nameSlash = if (name.startsWith('\\')) name else "\\$name"
        return getCommandsByName(nameSlash, file.project, GlobalSearchScope.fileScope(file))
    }

    /**
     * Get all commands that have the given names in a certain file. (with slash).
     */
    fun getCommandsByNames(file: PsiFile, vararg names: String): Collection<LatexCommands> {
        return getCommandsByNames(setOf(*names), file.project, GlobalSearchScope.fileScope(file))
    }
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StubIndexKey
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexMagicComment

/**
 * @author Hannah Schellekens
 */
object IndexKeys {

    val COMMANDS_KEY =
        StubIndexKey.createIndexKey<String, LatexCommands>("nl.hannahsten.texifyidea.commands")
    val INCLUDES_KEY =
        StubIndexKey.createIndexKey<String, LatexCommands>("nl.hannahsten.texifyidea.includes")
    val DEFINITIONS_KEY =
        StubIndexKey.createIndexKey<String, LatexCommands>("nl.hannahsten.texifyidea.definitions")
    val ENVIRONMENTS_KEY =
        StubIndexKey.createIndexKey<String, LatexEnvironment>("nl.hannahsten.texifyidea.environments")
    val MAGIC_COMMENTS_KEY =
        StubIndexKey.createIndexKey<String, LatexMagicComment>("nl.hannahsten.texifyidea.magiccomment")
    val LABELED_ENVIRONMENTS_KEY =
        StubIndexKey.createIndexKey<String, LatexEnvironment>("nl.hannahsten.texifyidea.parameterlabeledenvironments")
    val LABELED_COMMANDS_KEY =
        StubIndexKey.createIndexKey<String, LatexCommands>("nl.hannahsten.texifyidea.parameterlabeledcommands")
    val GLOSSARY_ENTRIES_KEY =
        StubIndexKey.createIndexKey<String, LatexCommands>("nl.hannahsten.texifyidea.glossaryentries")
}

package nl.hannahsten.texifyidea.index

import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.stubs.StubIndex
import com.intellij.psi.stubs.StubIndexKey
import com.intellij.refactoring.suggested.createSmartPointer
import nl.hannahsten.texifyidea.util.files.documentClassFileInProject
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.referencedFileSet

/**
 * @author Hannah Schellekens
 */
abstract class IndexUtilBase<T : PsiElement>(

    /**
     * The class of the elements that are stored in the index.
     */
    private val elementClass: Class<T>,

    /**
     * The key of the index.
     */
    private val indexKey: StubIndexKey<String, T>
) {

    /** Cache the index items to avoid unnecessary get actions from the index, which take a long time (50-100ms) even for a small index, which can be problematic if index is accessed many times per second. */
    var cache: MutableMap<Project, MutableMap<GlobalSearchScope, Collection<SmartPsiElementPointer<T>>>> = mutableMapOf()

    /**
     * Get all the items in the index in the given file set.
     * Consider using [nl.hannahsten.texifyidea.util.files.commandsInFileSet] where applicable.
     *
     * @param baseFile
     *          The file from which to look.
     */
    fun getItemsInFileSet(baseFile: PsiFile): Collection<T> {
        // Setup search set.
        val project = baseFile.project
        val searchFiles = baseFile.referencedFileSet().asSequence()
            .map { it.virtualFile }
            .toMutableSet()
        searchFiles.add(baseFile.virtualFile)

        // Add document class.
        val root = baseFile.findRootFile()
        val documentClass = root.documentClassFileInProject()
        if (documentClass != null) {
            searchFiles.add(documentClass.virtualFile)
            documentClass.referencedFileSet().asSequence()
                .forEach { searchFiles.add(it.virtualFile) }
        }

        // Search index.
        val scope = GlobalSearchScope.filesScope(project, searchFiles.filterNotNull())
        return getItems(project, scope)
    }

    /**
     * Get all the items in the index in the given file set, as well as the files where those items are.
     * Consider using [nl.hannahsten.texifyidea.util.files.commandsAndFilesInFileSet] where applicable.
     *
     * @param baseFile The file from which to look.
     * @return List of pairs consisting of a file and the items in that file.
     */
    fun getItemsAndFilesInFileSet(baseFile: PsiFile): List<Pair<PsiFile, Collection<T>>> {
        val result = mutableListOf<Pair<PsiFile, Collection<T>>>()

        // Find all files to search in
        val searchFiles = baseFile.referencedFileSet().toMutableSet()
        val documentclass = baseFile.findRootFile().documentClassFileInProject()
        if (documentclass != null) {
            searchFiles.add(documentclass)
        }

        for (file in searchFiles) {
            val scope = GlobalSearchScope.fileScope(file)
            result.add(Pair(file, getItems(baseFile.project, scope)))
        }

        return result
    }

    /**
     * Get all the items in the index that are in the given file.
     *
     * NOTE: this does not preserve the order of the commands.
     */
    fun getItems(file: PsiFile): Collection<T> {
        return if (!file.project.isDisposed) {
            getItems(file.project, GlobalSearchScope.fileScope(file))
        }
        else {
            emptySet()
        }
    }

    /**
     * Get all the items in the index that are in the given project.
     */
    fun getItems(project: Project) = getItems(project, GlobalSearchScope.projectScope(project))

    /**
     * Get all the items in the index.
     *
     * @param project
     *          The project instance.
     * @param scope
     *          The scope in which to search for the items.
     */
    fun getItems(project: Project, scope: GlobalSearchScope, useCache: Boolean = true): Collection<T> {
        if (useCache) {
            cache[project]?.get(scope)?.let { return runReadAction { it.mapNotNull { pointer -> pointer.element } } }
        }
        val result = getKeys(project).flatMap { getItemsByName(it, project, scope) }
        runReadAction { cache.getOrPut(project) { mutableMapOf() }[scope] = result.map { it.createSmartPointer() } }
        return result
    }

    /**
     * Get all the items in the index that have the given name.
     * WARNING This takes significant time because of index access. Be very careful about performance when calling it many times.
     *
     * @param name
     *          The name of the items to get.
     * @param project
     *          The project instance.
     * @param scope
     *          The scope in which to search for the items.
     */
    private fun getItemsByName(name: String, project: Project, scope: GlobalSearchScope): Collection<T> {
        try {
            return runReadAction { StubIndex.getElements(indexKey, name, project, scope, elementClass) }
        }
        catch (e: RuntimeExceptionWithAttachments) {
            // Ignore, because we've seen it only four times so far (#1375, #1446, #1591, #2086) but I fail to see how this would be a bug in TeXiFy.
            if (e.message?.contains("PSI and index do not match") == false) throw e
        }
        return emptySet()
    }

    /**
     * Get all the keys in the index.
     *
     * @param project
     *          The project instance.
     */
    private fun getKeys(project: Project): Array<String> {
        return if (!DumbService.isDumb(project) && !project.isDefault) {
            runReadAction { StubIndex.getInstance().getAllKeys(indexKey, project).toTypedArray() }
        }
        else {
            emptyArray()
        }
    }

    /**
     * Get the key of the index.
     */
    fun key() = indexKey
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import nl.hannahsten.texifyidea.grammar.LatexParserDefinition
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * @author Hannah Schellekens
 */
open class LatexCommandsIndex : StringStubIndexExtension<LatexCommands>() {

    companion object : IndexCommandsUtilBase(IndexKeys.COMMANDS_KEY)

    @Suppress("RedundantCompanionReference") // Avoid type checking issues
    override fun getKey() = Companion.key()

    override fun getVersion() = LatexParserDefinition.FILE.stubVersion
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import nl.hannahsten.texifyidea.grammar.LatexParserDefinition
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * @author Hannah Schellekens
 */
open class LatexDefinitionIndex : StringStubIndexExtension<LatexCommands>() {

    companion object : IndexCommandsUtilBase(IndexKeys.DEFINITIONS_KEY)

    @Suppress("RedundantCompanionReference")
    override fun getKey() = Companion.key()

    override fun getVersion() = LatexParserDefinition.FILE.stubVersion
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import nl.hannahsten.texifyidea.grammar.LatexParserDefinition
import nl.hannahsten.texifyidea.psi.LatexEnvironment

/**
 * This index contains all environments
 */
class LatexEnvironmentsIndex : StringStubIndexExtension<LatexEnvironment>() {

    companion object : IndexUtilBase<LatexEnvironment>(LatexEnvironment::class.java, IndexKeys.ENVIRONMENTS_KEY)

    @Suppress("RedundantCompanionReference")
    override fun getKey() = Companion.key()

    override fun getVersion() = LatexParserDefinition.FILE.stubVersion
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import nl.hannahsten.texifyidea.grammar.LatexParserDefinition
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * Index all (supported) glossary entry commands.
 */
class LatexGlossaryEntryIndex : StringStubIndexExtension<LatexCommands>() {

    companion object : IndexUtilBase<LatexCommands>(LatexCommands::class.java, IndexKeys.GLOSSARY_ENTRIES_KEY)

    @Suppress("RedundantCompanionReference") // Avoid type checking issues
    override fun getKey() = Companion.key()

    override fun getVersion() = LatexParserDefinition.FILE.stubVersion
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import nl.hannahsten.texifyidea.grammar.LatexParserDefinition
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * Index all commands that include other files, see [nl.hannahsten.texifyidea.util.getIncludeCommands].
 * @author Hannah Schellekens
 */
class LatexIncludesIndex : StringStubIndexExtension<LatexCommands>() {

    companion object : IndexCommandsUtilBase(IndexKeys.INCLUDES_KEY)

    @Suppress("RedundantCompanionReference")
    override fun getKey() = Companion.key()

    override fun getVersion() = LatexParserDefinition.FILE.stubVersion
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.PsiElement
import com.intellij.psi.stubs.IndexSink
import nl.hannahsten.texifyidea.highlighting.LatexAnnotator

fun <T : PsiElement> indexSinkOccurrence(indexSink: IndexSink, index: IndexUtilBase<T>, token: String) {
    // Clear cache to be sure that any update will be reflected (we don't know whether something will be added to the index or whether it's already in there)
    index.cache.clear()
    indexSink.occurrence(index.key(), token)
    LatexAnnotator.allUserDefinedCommands = emptyList()
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import com.intellij.psi.stubs.StubIndexKey
import nl.hannahsten.texifyidea.grammar.LatexParserDefinition
import nl.hannahsten.texifyidea.psi.LatexMagicComment

class LatexMagicCommentIndex : StringStubIndexExtension<LatexMagicComment>() {

    companion object : IndexUtilBase<LatexMagicComment>(LatexMagicComment::class.java, IndexKeys.MAGIC_COMMENTS_KEY)

    @Suppress("RedundantCompanionReference")
    override fun getKey(): StubIndexKey<String, LatexMagicComment> = Companion.key()

    override fun getVersion() = LatexParserDefinition.FILE.stubVersion
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import nl.hannahsten.texifyidea.grammar.LatexParserDefinition
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * This index contains commands that define a label in their  optional parameters.
 */
class LatexParameterLabeledCommandsIndex : StringStubIndexExtension<LatexCommands>() {

    companion object : IndexUtilBase<LatexCommands>(LatexCommands::class.java, IndexKeys.LABELED_COMMANDS_KEY)

    @Suppress("RedundantCompanionReference")
    override fun getKey() = Companion.key()

    override fun getVersion() = LatexParserDefinition.FILE.stubVersion
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import nl.hannahsten.texifyidea.grammar.LatexParserDefinition
import nl.hannahsten.texifyidea.psi.LatexEnvironment

/**
 * This index contains environments that define a label, i.e., environments which have
 * a label defined with an optional parameter.
 */
class LatexParameterLabeledEnvironmentsIndex : StringStubIndexExtension<LatexEnvironment>() {

    companion object : IndexUtilBase<LatexEnvironment>(LatexEnvironment::class.java, IndexKeys.LABELED_ENVIRONMENTS_KEY)

    @Suppress("RedundantCompanionReference")
    override fun getKey() = Companion.key()

    override fun getVersion() = LatexParserDefinition.FILE.stubVersion
}

package nl.hannahsten.texifyidea.inspections

import com.intellij.openapi.fileTypes.FileType
import nl.hannahsten.texifyidea.file.BibtexFileType
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.file.StyleFileType

/**
 * @author Hannah Schellekens
 */
enum class InsightGroup(

    /**
     * The name that gets displayed in the inspection settings.
     */
    val displayName: String,

    /**
     * The prefix of all internal inspection names.
     */
    val prefix: String,

    /**
     * The filetypes to which the inspection must be applied.
     */
    val fileTypes: Set<FileType>
) {

    LATEX("LaTeX", "Latex", setOf(LatexFileType, StyleFileType)),
    BIBTEX("BibTeX", "Bibtex", setOf(BibtexFileType));

    companion object {

        @JvmStatic
        fun byFileType(fileType: FileType): List<InsightGroup> {
            return values().filter { fileType in it.fileTypes }
        }
    }
}

package nl.hannahsten.texifyidea.inspections

import com.intellij.codeInspection.ex.InspectionToolRegistrar

/**
 * Map where each [InsightGroup] is mapped to all the relevant inspection ids.
 */
val ALL_TEXIFY_INSPECTIONS: Map<InsightGroup, List<String>> by lazy {
    val inspections = InspectionToolRegistrar.getInstance().createTools()
    val insightGroups = InsightGroup.values()
    HashMap<InsightGroup, List<String>>().apply {
        for (group in insightGroups) {
            val groupInspections = ArrayList<String>()
            for (inspection in inspections) {
                if (inspection.shortName.startsWith(group.prefix)) {
                    groupInspections.add(inspection.shortName.substring(group.prefix.length))
                }
            }
            put(group, groupInspections)
        }
    }
}

package nl.hannahsten.texifyidea.inspections

import com.intellij.psi.PsiElement
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy
import com.intellij.spellchecker.tokenizer.Tokenizer
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.lang.commands.Argument
import nl.hannahsten.texifyidea.lang.commands.Argument.Type
import nl.hannahsten.texifyidea.lang.commands.LatexMathCommand
import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.hasParent

/**
 * @author Hannah Schellekens
 */
class LatexSpellcheckingStrategy : SpellcheckingStrategy() {

    override fun isMyContext(psiElement: PsiElement): Boolean {
        return psiElement.language == LatexLanguage
    }

    override fun getTokenizer(psiElement: PsiElement): Tokenizer<*> {
        if (psiElement !is LeafPsiElement) {
            return EMPTY_TOKENIZER
        }

        if (psiElement.elementType == LatexTypes.COMMAND_TOKEN ||
            psiElement.elementType == LatexTypes.COMMAND_IFNEXTCHAR ||
            psiElement.elementType == LatexTypes.COMMENT_TOKEN ||
            isBeginEnd(psiElement) ||
            psiElement.hasParent(LatexOptionalParam::class)
        ) {
            return EMPTY_TOKENIZER
        }

        val argument = getArgument(psiElement)
        if (
            argument == null &&
            psiElement.elementType == LatexTypes.NORMAL_TEXT_WORD &&
            // Exclude text in parameters by default, unless we know it contains text (e.g. \section)
            !psiElement.hasParent(LatexParameterText::class)
        ) {
            return TEXT_TOKENIZER
        }

        if (argument?.type == Type.TEXT) {
            return TEXT_TOKENIZER
        }

        return EMPTY_TOKENIZER
    }

    private fun isBeginEnd(element: PsiElement): Boolean {
        var elt: PsiElement? = PsiTreeUtil.getParentOfType(element, LatexBeginCommand::class.java)
        if (elt != null) {
            return true
        }

        elt = PsiTreeUtil.getParentOfType(element, LatexEndCommand::class.java)
        return elt != null
    }

    /**
     * Get the argument (with type) from TeXiFy knowledge that corresponds with the current psi element.
     */
    private fun getArgument(leaf: LeafPsiElement): Argument? {
        val parent = PsiTreeUtil.getParentOfType(leaf, LatexCommands::class.java) ?: return null

        val arguments = getArguments(parent.commandToken.text.substring(1)) ?: return null

        val realParams = parent.getRequiredParameters()
        // Note that a leaf may be only part of a parameter
        val parameterIndex = realParams.indexOf(leaf.firstParentOfType(LatexParameterText::class)?.text)
        return if (parameterIndex < 0 || parameterIndex >= arguments.size) {
            null
        }
        else arguments[parameterIndex]
    }

    private fun getArguments(commandName: String): Array<out Argument>? {
        val cmdHuh = LatexRegularCommand[commandName]
        if (cmdHuh != null) {
            return cmdHuh.first().arguments
        }

        val mathCmdHuh = LatexMathCommand[commandName] ?: return null

        return mathCmdHuh.first().arguments
    }
}

/*
 * Copyright 2000-2009 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nl.hannahsten.texifyidea.inspections

import com.intellij.codeInsight.FileModificationService
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.safeDelete.SafeDeleteHandler

/**
 * Source: com.intellij.codeInsight.daemon.impl.quickfix.SafeDeleteFix
 */
open class SafeDeleteFix(element: PsiElement) : LocalQuickFixAndIntentionActionOnPsiElement(element) {

    override fun getText(): String {
        val startElement = startElement
        return "Safe delete " + startElement.text
    }

    override fun getFamilyName(): String {
        return "Safe delete"
    }

    override fun invoke(
        project: Project,
        file: PsiFile,
        editor: Editor?,
        startElement: PsiElement,
        endElement: PsiElement
    ) {
        if (!FileModificationService.getInstance().prepareFileForWrite(file)) return
        val elements = arrayOf(startElement)
        SafeDeleteHandler.invoke(project, elements, true)
    }

    override fun startInWriteAction(): Boolean {
        return false
    }
}

package nl.hannahsten.texifyidea.inspections

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.SuppressQuickFix
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.createSmartPointer
import com.intellij.util.SmartList
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.lang.magic.*
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexGroup
import nl.hannahsten.texifyidea.psi.LatexMathEnvironment
import nl.hannahsten.texifyidea.util.parser.isComment
import nl.hannahsten.texifyidea.util.parser.name
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * @author Hannah Schellekens
 */
abstract class TexifyInspectionBase : LocalInspectionTool() {

    /**
     * The inspectionGroup the inspection falls under.
     */
    abstract val inspectionGroup: InsightGroup

    /**
     * A unique string indentifier for the inspection.
     */
    abstract val inspectionId: String

    /**
     * The magic comment scopes that should not have a [SuppressQuickFix].
     */
    open val ignoredSuppressionScopes: Set<MagicCommentScope> = emptySet()

    /**
     * All the scopes whose suppression quick fix should target the _parent/outer_ PsiElement.
     *
     * This is useful in the cases like `\ref{...}`, where if you want to supress for group, you do not want the
     * quick fix to result in `\ref{ %! Suppress = ... }`, but rather to target the group in which the
     * `\ref` is contained (if it exists).
     */
    open val outerSuppressionScopes: Set<MagicCommentScope> = emptySet()

    /**
     * Gets called whenever the file should be inspected.
     *
     * @param file
     *          The file to inspect.
     * @param manager
     *          InspectionManager to ask for ProblemDescriptor's from.
     * @param isOntheFly
     *          `true` if called during on the fly editor highlighting. Called from Inspect Code action otherwise.
     */
    abstract fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor>

    /**
     * Checks if the element is in the correct context for the inspection to be applied.
     *
     * @return `true` if the inspection is allowed for this element in its context, `false` otherwise.
     */
    open fun checkContext(element: PsiElement) = element.isComment().not() && element.isSuppressed().not()

    /**
     * Creates an empty list to store problem descriptors in.
     */
    protected open fun descriptorList(): MutableList<ProblemDescriptor> = SmartList()

    /**
     * Checks whether the inspection must be suppressed (`true`) or not (`false`) based on the position of the given
     * PsiElement.
     */
    protected open fun PsiElement.isSuppressed(): Boolean {
        return magicComment()?.containsPair("suppress", inspectionId) == true ||
            allParentMagicComments().containsPair("suppress", inspectionId)
    }

    override fun getBatchSuppressActions(element: PsiElement?): Array<SuppressQuickFix> {
        val result = ArrayList<SuppressionFixBase>()

        element?.let { elt ->
            elt.containingFile?.let { result.add(FileSuppressionFix(it.createSmartPointer())) }

            elt.suppressionElement<LatexEnvironment>(MagicCommentScope.ENVIRONMENT)?.let {
                result.add(EnvironmentSuppressionFix(it))
            }
            elt.suppressionElement<LatexMathEnvironment>(MagicCommentScope.MATH_ENVIRONMENT)?.let {
                result.add(MathEnvironmentSuppressionFix(it))
            }
            elt.suppressionElement<LatexCommands>(MagicCommentScope.COMMAND)?.let {
                result.add(CommandSuppressionFix(it))
            }
            elt.suppressionElement<LatexGroup>(MagicCommentScope.GROUP)?.let {
                result.add(GroupSuppressionFix(it))
            }
        }

        return result.filter { it.suppressionScope !in ignoredSuppressionScopes }.toTypedArray()
    }

    /**
     * Get the element relative to `this` element that must be targeted by the suppression quick fix given the
     * magic comment scope.
     */
    private inline fun <reified Psi : PsiElement> PsiElement.suppressionElement(scope: MagicCommentScope): Psi? {
        val parent = parentOfType(Psi::class)

        return if (scope in outerSuppressionScopes) {
            parent?.parentOfType(Psi::class)
        }
        else parent
    }

    override fun getShortName() = inspectionGroup.prefix + inspectionId

    override fun getGroupDisplayName() = inspectionGroup.displayName

    override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array<ProblemDescriptor>? {
        // Only inspect the right file types.
        if (file.fileType !in inspectionGroup.fileTypes) {
            return null
        }

        // Check for file inspection suppression seperately as it is relatively cheap.
        // Do not execute the (relative expensive) inspection when it is suppressed globally.
        if (file.isSuppressed()) {
            return null
        }

        return inspectFile(file, manager, isOnTheFly)
            .filter { checkContext(it.psiElement) }
            .toTypedArray()
    }

    /**
     * @author Hannah Schellekens
     */
    private abstract inner class SuppressionFixBase(val targetElement: SmartPsiElementPointer<out PsiElement>) : SuppressQuickFix {

        /**
         * The scope to which to apply the suppression.
         */
        abstract val suppressionScope: MagicCommentScope

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val magicComment = MutableMagicComment<String, String>()
            magicComment.addValue(DefaultMagicKeys.SUPPRESS, inspectionId)
            targetElement.element?.addMagicCommentToPsiElement(magicComment)
        }

        override fun isAvailable(project: Project, context: PsiElement): Boolean {
            return context.containingFile.fileType == LatexFileType
        }

        override fun isSuppressAll() = false
    }

    /**
     * @author Hannah Schellekens
     */
    private inner class FileSuppressionFix(val file: SmartPsiElementPointer<PsiFile>) : SuppressionFixBase(file) {

        override val suppressionScope = MagicCommentScope.FILE

        override fun getFamilyName() = "Suppress for file '${file.element?.name}'"
    }

    /**
     * @author Hannah Schellekens
     */
    private inner class EnvironmentSuppressionFix(parentEnvironment: LatexEnvironment) : SuppressionFixBase(parentEnvironment.createSmartPointer()) {

        /**
         * The name of the environment to suppress, or `null` when there is no environment name available.
         */
        private val environmentName: String? = parentEnvironment.name()?.text

        override val suppressionScope = MagicCommentScope.ENVIRONMENT

        override fun getFamilyName() = "Suppress for environment '$environmentName'"

        override fun isAvailable(project: Project, context: PsiElement): Boolean {
            return environmentName != null && super.isAvailable(project, context)
        }
    }

    /**
     * @author Hannah Schellekens
     */
    private inner class MathEnvironmentSuppressionFix(parentMathEnvironment: LatexMathEnvironment) : SuppressionFixBase(parentMathEnvironment.createSmartPointer()) {

        override val suppressionScope = MagicCommentScope.MATH_ENVIRONMENT

        override fun getFamilyName() = "Suppress for math environment"
    }

    /**
     * @author Hannah Schellekens
     */
    private inner class CommandSuppressionFix(parentCommand: LatexCommands) : SuppressionFixBase(parentCommand.createSmartPointer()) {

        /**
         * The name of the command to suppress, or `null` when there is no command name available.
         */
        private val commandToken: String? = parentCommand.commandToken.text

        override val suppressionScope = MagicCommentScope.COMMAND

        override fun getFamilyName() = "Suppress for command '$commandToken'"

        override fun isAvailable(project: Project, context: PsiElement): Boolean {
            return commandToken != null && super.isAvailable(project, context)
        }
    }

    /**
     * @author Hannah Schellekens
     */
    private inner class GroupSuppressionFix(parentGroup: LatexGroup) : SuppressionFixBase(parentGroup.createSmartPointer()) {

        override val suppressionScope = MagicCommentScope.GROUP

        override fun getFamilyName() = "Suppress for group"
    }
}

package nl.hannahsten.texifyidea.inspections

import com.intellij.ui.IdeBorderFactory
import org.jdom.Element
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTextArea

/**
 * An inspection that carries an extra text area that serves as line input.
 *
 * @author Hannah Schellekens
 */
abstract class TexifyLineOptionsInspection(val title: String) : TexifyInspectionBase() {

    /**
     * TextArea that contains all the line options.
     */
    private val txtaOptions = JTextArea()

    /**
     * Key that is used to read and write from settings.
     */
    private val attributeKey = "texify.inspection.latex.$shortName.textArea"

    /**
     * All the nonempty lines in the text area.
     */
    protected val lines: List<String>
        get() = txtaOptions.text.trim().split("\n").asSequence()
            .map { it.trim() }
            .filter { it.isNotBlank() }
            .toList()

    override fun createOptionsPanel(): JComponent {
        val panel = JPanel()
        panel.layout = BoxLayout(panel, BoxLayout.Y_AXIS)
        panel.add(txtaOptions)
        panel.border = IdeBorderFactory.createTitledBorder(title)
        return panel
    }

    override fun writeSettings(node: Element) {
        node.setAttribute(attributeKey, txtaOptions.text)
        super.writeSettings(node)
    }

    override fun readSettings(node: Element) {
        val value = node.getAttributeValue(attributeKey)
        if (value != null) {
            txtaOptions.text = value
        }

        super.readSettings(node)
    }
}

package nl.hannahsten.texifyidea.inspections

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.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.psi.LatexRawText
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.hasParent
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.parser.isComment
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
abstract class TexifyRegexInspection(

    /**
     * The display name of the inspection.
     */
    val inspectionDisplayName: String,

    /**
     * The short name of the inspection (same name as the html info file).
     */
    override val inspectionId: String,

    /**
     * The regex pattern that targets the text for the inspection.
     */
    val pattern: Pattern,

    /**
     * The error message that shows up when you hover over the problem descriptor.
     */
    val errorMessage: (Matcher) -> String,

    /**
     * What to replace in the document.
     */
    val replacement: (Matcher, PsiFile) -> String = { _, _ -> "" },

    /**
     * Fetches different groups from a matcher.
     */
    val groupFetcher: (Matcher) -> List<String> = { listOf() },

    /**
     * The range in the found pattern that must be replaced.
     */
    val replacementRange: (Matcher) -> IntRange = { it.start()..it.end() },

    /**
     * The highlight level of the problem, GENERIC_ERROR_OR_WARNING by default.
     */
    val highlight: ProblemHighlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING,

    /**
     * Name of the quick fix.
     */
    val quickFixName: (Matcher) -> String = { "Do fix pls" },

    /**
     * `true` when the inspection is in mathmode, `false` (default) when not in math mode.
     */
    val mathMode: Boolean = false,

    /**
     * Predicate that if `true`, cancels the inspection.
     */
    val cancelIf: (Matcher, PsiFile) -> Boolean = { _, _ -> false },

    /**
     * Provides the text ranges that mark the squiggly warning thingies.
     */
    val highlightRange: (Matcher) -> TextRange = { TextRange(it.start(), it.end()) },

    /**
     * In which inspection inspectionGroup the inspection lies.
     */
    override val inspectionGroup: InsightGroup = InsightGroup.LATEX

) : TexifyInspectionBase() {

    companion object {

        /**
         * Get the IntRange that spans the inspectionGroup with the given id.
         */
        fun Matcher.groupRange(groupId: Int): IntRange = start(groupId)..end(groupId)

        /**
         * Checks if the matched element is a child of a certain PsiElement.
         */
        inline fun <reified T : PsiElement> isInElement(matcher: Matcher, file: PsiFile): Boolean {
            val element = file.findElementAt(matcher.start()) ?: return false
            return element.hasParent(T::class)
        }
    }

    override fun getDisplayName() = inspectionDisplayName

    override fun checkContext(element: PsiElement) = element.isSuppressed().not() && element.firstParentOfType(LatexRawText::class) == null

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        // Find all patterns.
        val text = file.text
        val matcher = pattern.matcher(text)

        return if (!isOntheFly && runForWholeFile()) {
            inspectFileNotOnTheFly(file, manager, matcher)
        }
        else {
            inspectFileOnTheFly(file, manager, matcher)
        }
    }

    /**
     * The file is inspected 'not on the fly' when the 'fix all problems in file' menu is used.
     * If that is the case and we need to run the inspection for the
     * whole file at once, we create only one problem descriptor for
     * the whole file, which has all the problem locations.
     * Then the problem descriptor will be applied and we can take
     * control of handling the displacements of the fixed regex locations ourselves.
     * The reason we do not always want one problem descriptor is
     * that it would be rendered in IntelliJ as one global warning
     * bar at the top of the file, instead of at the correct locations.
     */
    private fun inspectFileNotOnTheFly(file: PsiFile, manager: InspectionManager, matcher: Matcher): MutableList<ProblemDescriptor> {
        val replacementRanges = arrayListOf<IntRange>()
        val replacements = arrayListOf<String>()
        val groups = arrayListOf<List<String>>()

        var quickFixName = ""
        var errorMessage = ""

        // For each pattern, just save the replacement location and value
        // We use the fact that the matcher finds issues in increasing order when applying fixes
        while (matcher.find()) {
            // Pre-checks.
            if (cancelIf(matcher, file)) {
                continue
            }

            groups.add(groupFetcher(matcher))

            val range = replacementRange(matcher)
            val replacementContent = replacement(matcher, file)

            // Just take the last error/name even if it may not apply to all problems at once
            // (they may each have a different error message) because we have to choose only one for the one problem descriptor.
            errorMessage = errorMessage(matcher)
            quickFixName = quickFixName(matcher)

            // Correct context.
            val element = file.findElementAt(matcher.start()) ?: continue
            if (!checkContext(matcher, element)) {
                continue
            }

            replacementRanges.add(range)
            replacements.add(replacementContent)
        }

        if (replacementRanges.isNotEmpty()) {
            // We cannot give one TextRange because there are multiple,
            // but it does not matter since the user won't see this anyway.
            val problemDescriptor = manager.createProblemDescriptor(
                file,
                null as TextRange?,
                errorMessage,
                highlight,
                false,
                RegexFixes(
                    quickFixName,
                    replacements,
                    replacementRanges,
                    groups,
                    this::applyFixes
                )
            )

            return mutableListOf(problemDescriptor)
        }
        else {
            return mutableListOf()
        }
    }

    /**
     * Inspect the file and create a list of all problem descriptors.
     */
    private fun inspectFileOnTheFly(file: PsiFile, manager: InspectionManager, matcher: Matcher): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        while (matcher.find()) {
            // Pre-checks.
            if (cancelIf(matcher, file)) {
                continue
            }

            val groups = groupFetcher(matcher)
            val textRange = highlightRange(matcher)
            val startOffset = if (textRange.startOffset < 0) 0 else textRange.startOffset
            val endOffset = if (textRange.endOffset > file.textLength) file.textLength else textRange.endOffset
            val textRangeFixed = TextRange(startOffset, endOffset)

            val range = replacementRange(matcher)
            val error = errorMessage(matcher)
            val quickFix = quickFixName(matcher)
            val replacementContent = replacement(matcher, file)

            // Correct context.
            val element = file.findElementAt(matcher.start()) ?: continue
            if (!checkContext(matcher, element)) {
                continue
            }

            descriptors.add(
                manager.createProblemDescriptor(
                    file,
                    textRangeFixed,
                    error,
                    highlight,
                    true,
                    RegexFixes(
                        quickFix,
                        arrayListOf(replacementContent),
                        arrayListOf(range),
                        arrayListOf(groups),
                        this::applyFixes
                    )
                )
            )
        }

        return descriptors
    }

    /**
     * Checks if the element is in the correct context.
     *
     * By default checks for math mode.
     *
     * @return `true` if the inspection is allowed in the context, `false` otherwise.
     */
    open fun checkContext(matcher: Matcher, element: PsiElement): Boolean {
        if (element.isComment()) {
            return false
        }

        return mathMode == element.inMathContext() && checkContext(element)
    }

    /**
     * We assume the quickfix of this inspection replaces text (like <<) by
     * content with a different length (like \ll), so we have to make
     * sure it is run for the whole file at once.
     * It can be disabled by overriding this method.
     */
    override fun runForWholeFile(): Boolean {
        return true
    }

    /**
     * Replaces all text in the replacementRange by the correct replacement.
     *
     * @return The total increase in document length, e.g. if << is replaced by
     * \ll and \usepackage{amsmath} is added then the total increase is 3 + 20 - 2.
     */
    open fun applyFix(descriptor: ProblemDescriptor, replacementRange: IntRange, replacement: String, groups: List<String>): Int {
        val file = descriptor.psiElement as PsiFile
        val document = file.document() ?: return 0

        document.replaceString(replacementRange.first, replacementRange.last, replacement)

        return replacement.length - replacementRange.length
    }

    /**
     * Replaces all text for all replacementRanges by the correct replacements.
     *
     * We assume the quickfix of an inspection performs replacements which could
     * have a different length than the original text, the inspection
     * should override [runForWholeFile] to explicitly refute this assumption.
     *
     * @param groups Regex groups as matched by the regex matcher.
     * @param replacementRanges These replacement ranges have to be ordered increasingly and have to be non-overlapping.
     */
    open fun applyFixes(
        descriptor: ProblemDescriptor,
        replacementRanges: List<IntRange>,
        replacements: List<String>,
        groups: List<List<String>>
    ) {
        val fixFunction = { replacementRange: IntRange, replacement: String, group: List<String> -> applyFix(descriptor, replacementRange, replacement, group) }
        applyFixes(fixFunction, replacementRanges, replacements, groups)
    }

    /**
     * See [applyFixes].
     */
    open fun applyFixes(
        fixFunction: (IntRange, String, List<String>) -> Int,
        replacementRanges: List<IntRange>,
        replacements: List<String>,
        groups: List<List<String>>
    ) {
        require(replacementRanges.size == replacements.size) { "The number of replacement values has to equal the number of ranges of those replacements." }

        // Remember how much a replacement changed the locations of the fixes still to be applied.
        // This is cumulative, but may be negative.
        var accumulatedDisplacement = 0

        // Loop over all fixes manually, in order to fix the regex locations
        for (i in replacements.indices) {
            val replacementRange = replacementRanges[i]
            val replacement = replacements[i]

            val newRange = IntRange(replacementRange.first + accumulatedDisplacement, replacementRange.last + accumulatedDisplacement)
            val replacementLength = fixFunction(newRange, replacement, groups[i])

            // Fix the locations of the next fixes
            accumulatedDisplacement += replacementLength
        }
    }

    /**
     * @author Hannah Schellekens
     */
    open class RegexFixes(
        private val fixName: String,
        val replacements: List<String>,
        val replacementRanges: List<IntRange>,
        val groups: List<List<String>>,
        val fixFunction: (ProblemDescriptor, List<IntRange>, List<String>, List<List<String>>) -> Unit
    ) : LocalQuickFix {

        override fun getFamilyName(): String = fixName

        override fun applyFix(project: Project, problemDescriptor: ProblemDescriptor) {
            fixFunction(problemDescriptor, replacementRanges, replacements, groups)
        }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.util.parentOfType
import com.intellij.refactoring.rename.RenameProcessor
import com.intellij.refactoring.rename.inplace.MemberInplaceRenamer
import nl.hannahsten.texifyidea.psi.LatexCommandWithParams
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.labels.findLatexAndBibtexLabelStringsInFileSet
import kotlin.math.max

abstract class LatexAddLabelIntention(name: String) : TexifyIntentionBase(name) {

    /**
     * This class handles the rename of a label parameter after insertion
     */
    private class LabelInplaceRenamer(
        elementToRename: PsiNamedElement,
        editor: Editor,
        private val prefix: String,
        initialName: String,
        private val endMarker: RangeMarker
    ) : MemberInplaceRenamer(elementToRename, null, editor, initialName, initialName) {

        override fun getRangeToRename(element: PsiElement): TextRange {
            // The label prefix is given by convention and not renamed
            return TextRange(prefix.length, element.textLength)
        }

        override fun createRenameProcessor(element: PsiElement?, newName: String?): RenameProcessor {
            // Automatically prepend the prefix
            return super.createRenameProcessor(element, "$prefix$newName")
        }

        override fun moveOffsetAfter(success: Boolean) {
            super.moveOffsetAfter(success)
            myEditor.caretModel.moveToOffset(endMarker.endOffset)
        }

        override fun restoreCaretOffsetAfterRename() {
            // Dispose the marker like the parent method does, but do not move the caret. We already moved it in
            // moveOffsetAfter
            if (myBeforeRevert != null) {
                myBeforeRevert.dispose()
            }
        }
    }

    protected inline fun <reified T : PsiElement> findTarget(editor: Editor?, file: PsiFile?): T? {
        val offset = editor?.caretModel?.offset ?: return null
        val element = file?.findElementAt(offset) ?: return null
        // Also check one position back, because we want it to trigger in \section{a}<caret>
        return element as? T ?: element.parentOfType()
            ?: file.findElementAt(max(0, offset - 1)) as? T
            ?: file.findElementAt(max(0, offset - 1))?.parentOfType()
    }

    protected fun getUniqueLabelName(base: String, prefix: String, file: PsiFile): LabelWithPrefix {
        val labelBase = "$prefix:$base"
        val allLabels = file.findLatexAndBibtexLabelStringsInFileSet()
        val fullLabel = appendCounter(labelBase, allLabels)
        return LabelWithPrefix(prefix, fullLabel.substring(prefix.length + 1))
    }

    /**
     * Keeps adding a counter behind the label until there is no other label with that name.
     */
    private fun appendCounter(label: String, allLabels: Set<String>): String {
        var counter = 2
        var candidate = label

        while (allLabels.contains(candidate)) {
            candidate = label + (counter++)
        }

        return candidate
    }

    data class LabelWithPrefix(val prefix: String, val base: String) {

        val prefixText = "$prefix:"
        val labelText = "$prefix:$base"
    }

    protected fun createLabelAndStartRename(
        editor: Editor,
        project: Project,
        command: LatexCommandWithParams,
        label: LabelWithPrefix,
        moveCaretTo: RangeMarker
    ) {
        val helper = LatexPsiHelper(project)
        val parameter = helper.setOptionalParameter(command, "label", "{${label.labelText}}")
        PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)

        // setOptionalParameter should create an appropriate optionalArgument node with label={text} in it
        val parameterText =
            parameter?.keyValValue?.keyValContentList?.firstOrNull()?.parameterGroup?.parameterGroupText?.parameterTextList?.firstOrNull()
                ?: throw AssertionError("parameter created by setOptionalParameter does not have the right structure")
        // Move the caret onto the label
        editor.caretModel.moveToOffset(parameterText.textOffset + label.prefix.length + 1)
        val renamer = LabelInplaceRenamer(parameterText, editor, label.prefixText, label.base, moveCaretTo)
        renamer.performInplaceRefactoring(LinkedHashSet())
    }

    override fun startInWriteAction() = true

    // Not clear to me why the default implementation does not work, but this avoids the "Intention preview fallback is used for action" error
    override fun generatePreview(project: Project, editor: Editor, file: PsiFile): IntentionPreviewInfo {
        invoke(project, editor, file)
        return IntentionPreviewInfo.DIFF
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TextExpression
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexNoMathContent
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.settings.conventions.LabelConventionType
import nl.hannahsten.texifyidea.settings.conventions.TexifyConventionsSettingsManager
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.formatAsLabel
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * @author Hannah Schellekens
 */
open class LatexAddLabelToCommandIntention(val command: SmartPsiElementPointer<LatexCommands>? = null) :
    LatexAddLabelIntention("Add label to command") {

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (file?.isLatexFile() == false) {
            return false
        }

        val targetName = findTarget<LatexCommands>(editor, file)?.name
        val conventionSettings = TexifyConventionsSettingsManager.getInstance(project).getSettings()

        return conventionSettings.getLabelConvention(targetName, LabelConventionType.COMMAND)?.enabled ?: false
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null) {
            return
        }

        // When no this.command is provided, use the command at the caret as the best guess.
        val command: LatexCommands = this.command?.element
            ?: findTarget(editor, file)
            ?: return

        val conventionSettings = TexifyConventionsSettingsManager.getInstance(project).getSettings()
        val prefix = conventionSettings.getLabelConvention(command.name, LabelConventionType.COMMAND)?.prefix ?: return

        val factory = LatexPsiHelper(project)

        // For sections we can infer a reasonable label name from the required parameter
        if (CommandMagic.sectionMarkers.contains(command.name)) {
            val required = command.getRequiredParameters()
            // Section commands should all have a required parameter
            val labelString: String = required.getOrNull(0) ?: return
            val createdLabel = getUniqueLabelName(
                labelString.formatAsLabel(),
                prefix, command.containingFile
            )

            // Insert label
            val commandContent = command.firstParentOfType(LatexNoMathContent::class) ?: return
            val labelCommand =
                commandContent.parent.addAfter(factory.createLabelCommand(createdLabel.labelText), commandContent)

            // Adjust caret offset.
            val caret = editor.caretModel
            caret.moveToOffset(labelCommand.endOffset())
        }
        else {
            if (CommandMagic.labelAsParameter.contains(command.name)) {
                // Create a label parameter and initiate the rename process
                val createdLabel = getUniqueLabelName(
                    command.name!!.replace("\\", ""),
                    prefix, command.containingFile
                )
                val endMarker =
                    editor.document.createRangeMarker(command.startOffset, command.endOffset())
                createLabelAndStartRename(editor, project, command, createdLabel, endMarker)
            }
            else {
                // Insert and start the \label live template
                editor.caretModel.moveToOffset(command.endOffset())
                val template = TemplateImpl("", "\\label{$prefix:\$__Variable0\$}", "")
                template.addVariable(TextExpression(""), true)
                TemplateManager.getInstance(editor.project).startTemplate(editor, template)
            }
        }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.CAPTION
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.psi.getEnvironmentName
import nl.hannahsten.texifyidea.settings.conventions.LabelConventionType
import nl.hannahsten.texifyidea.settings.conventions.TexifyConventionsSettingsManager
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.formatAsLabel
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.endOffset

open class LatexAddLabelToEnvironmentIntention(val environment: SmartPsiElementPointer<LatexEnvironment>? = null) :
    LatexAddLabelIntention("Add label to environment") {

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (file?.isLatexFile() == false) {
            return false
        }

        val targetName = findTarget<LatexEnvironment>(editor, file)?.getEnvironmentName()
        val conventionSettings = TexifyConventionsSettingsManager.getInstance(project).getSettings()
        return conventionSettings.getLabelConvention(targetName, LabelConventionType.ENVIRONMENT)?.enabled
            ?: false
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null) return

        val environment = this.environment?.element
            ?: findTarget(editor, file)
            ?: return

        val helper = LatexPsiHelper(project)

        // Determine label name.
        val conventionSettings = TexifyConventionsSettingsManager.getInstance(project).getSettings()
        val prefix =
            conventionSettings.getLabelConvention(environment.getEnvironmentName(), LabelConventionType.ENVIRONMENT)?.prefix
                ?: return

        val createdLabel = getUniqueLabelName(
            environment.getEnvironmentName().formatAsLabel(),
            prefix, environment.containingFile
        )

        if (EnvironmentMagic.labelAsParameter.contains(environment.getEnvironmentName())) {
            val endMarker = editor.document.createRangeMarker(environment.startOffset, environment.endOffset())
            createLabelAndStartRename(editor, project, environment.beginCommand, createdLabel, endMarker)
        }
        else {
            // in a float environment the label must be inserted after a caption
            val labelCommand = helper.addToContent(
                environment, helper.createLabelCommand(createdLabel.labelText),
                environment.environmentContent?.childrenOfType<LatexCommands>()
                    ?.findLast { c -> c.name == CAPTION.commandWithSlash }
            )

            // Adjust caret offset
            val caretModel = editor.caretModel
            caretModel.moveToOffset(labelCommand.endOffset())
        }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.util.parser.endCommand
import nl.hannahsten.texifyidea.util.parser.environmentName
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.replaceString

/**
 * @author Hannah Schellekens
 */
open class LatexDisplayMathIntention : TexifyIntentionBase("Change equation*/displaymath environment to '\\[..\\]'") {

    companion object {

        private val affectedEnvironments = setOf("displaymath", "equation*")
    }

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (editor == null || file == null || !file.isLatexFile()) {
            return false
        }

        val selected = file.findElementAt(editor.caretModel.offset) ?: return false
        val begin = selected.parentOfType(LatexBeginCommand::class) ?: return false
        val name = begin.environmentName() ?: return false

        return affectedEnvironments.contains(name)
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null || !file.isLatexFile()) {
            return
        }

        val selected = file.findElementAt(editor.caretModel.offset) ?: return
        val begin = selected.parentOfType(LatexBeginCommand::class) ?: return
        val end = begin.endCommand() ?: return

        val document = editor.document
        runWriteAction {
            document.replaceString(end.textRange, "\\]")
            document.replaceString(begin.textRange, "\\[")
        }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.psi.LatexDisplayMath
import nl.hannahsten.texifyidea.psi.LatexInlineMath
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.parser.hasParent
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * @author Hannah Schellekens
 */
open class LatexInlineDisplayToggleIntention : TexifyIntentionBase("Toggle inline/display math mode") {

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (editor == null || file == null || !file.isLatexFile()) {
            return false
        }

        val element = file.findElementAt(editor.caretModel.offset) ?: return false
        return element.hasParent(LatexInlineMath::class) || element.hasParent(LatexDisplayMath::class)
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null || !file.isLatexFile()) {
            return
        }

        val element = file.findElementAt(editor.caretModel.offset) ?: return
        val inline = element.parentOfType(LatexInlineMath::class)
        if (inline != null) {
            applyForInlineMath(editor, inline)
        }
        else {
            applyForDisplayMath(editor, element.parentOfType(LatexDisplayMath::class) ?: return)
        }
    }

    private fun applyForInlineMath(editor: Editor, inline: LatexInlineMath) {
        val document = editor.document
        val indent = document.lineIndentationByOffset(inline.textOffset)
        val endLength = inline.inlineMathEnd?.textLength ?: 0
        val text = inline.text.trimRange(inline.inlineMathStart.textLength, endLength).trim()

        runWriteAction {
            val extra = if (document.getText(TextRange.from(inline.endOffset(), 1)) == " ") {
                1
            }
            else 0

            val result = "\n\\[\n    $text\n\\]\n".replace("\n", "\n$indent")
            document.replaceString(inline.textOffset, inline.endOffset() + extra, result)
            editor.caretModel.moveToOffset(inline.textOffset + result.length)
        }
    }

    private fun applyForDisplayMath(editor: Editor, display: LatexDisplayMath) {
        val document = editor.document
        val indent = document.lineIndentationByOffset(display.textOffset)
        val whitespace = indent.length + 1
        val text = display.text.trimRange(2, 2).trim()

        runWriteAction {
            val leading = if (document.getText(TextRange.from(display.textOffset - whitespace - 1, 1)) != " ") " " else ""
            val trailing = if (document.getText(TextRange.from(display.endOffset() + whitespace, 1)) != " ") " " else ""

            val result = "$leading${'$'}$text${'$'}$trailing"
                .replace("\n", " ")
                .replace(Regex("\\s+"), " ")
            document.replaceString(display.textOffset - whitespace, display.endOffset() + whitespace, result)
        }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleSettings
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.endCommand
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.parser.environmentName
import nl.hannahsten.texifyidea.util.parser.parentOfType

class LatexInsertFormatterCommentsIntention : TexifyIntentionBase("Insert comments to disable the formatter.") {

    private val onTag = CodeStyleSettings.getDefaults().FORMATTER_ON_TAG
    private val offTag = CodeStyleSettings.getDefaults().FORMATTER_OFF_TAG

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        val beginName = file?.findElementAt(editor?.caretOffset() ?: return false)
            ?.parentOfType(LatexBeginCommand::class)
            ?.environmentName()
            ?: return false
        return EnvironmentMagic.isProbablyVerbatim(beginName)
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        val beginCommand = file?.findElementAt(editor?.caretOffset() ?: return)
            ?.parentOfType(LatexBeginCommand::class)
        val endCommand = beginCommand?.endCommand() ?: return

        val indent: String = editor?.document?.lineIndentationByOffset(beginCommand.textOffset) ?: return
        val offComment = "% $offTag\n$indent"
        val onComment = "\n$indent% $onTag"

        editor.insertAndMove(beginCommand.textOffset, offComment)
        editor.insertAndMove(endCommand.endOffset() + offComment.length, onComment)
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.lang.Language
import com.intellij.lang.LanguageParserDefinitions
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.PopupChooserBuilder
import com.intellij.psi.PsiFile
import com.intellij.psi.injection.Injectable
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.components.JBList
import com.intellij.util.ui.EmptyIcon
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.lang.magic.DefaultMagicKeys
import nl.hannahsten.texifyidea.lang.magic.MutableMagicComment
import nl.hannahsten.texifyidea.lang.magic.addMagicComment
import nl.hannahsten.texifyidea.lang.magic.magicComment
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.SwingConstants

/**
 * @author Sten Wessel
 */
class LatexLanguageInjectionIntention : TexifyIntentionBase("Inject language") {

    override fun getFamilyName() = "Inject language"

    override fun getText() = "Permanently inject language in environment"

    override fun startInWriteAction() = true

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (file !is LatexFile || editor == null) {
            return false
        }

        val element = file.findElementAt(editor.caretModel.offset) ?: return false

        val env = element.firstParentOfType(LatexEnvironment::class) ?: return false

        return !env.magicComment().containsKey(DefaultMagicKeys.INJECT_LANGUAGE)
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (file !is LatexFile || editor == null) {
            return
        }

        chooseLanguage(editor) { language ->
            WriteCommandAction.runWriteCommandAction(project) {
                inject(file, editor, language)
            }
        }
    }

    override fun generatePreview(project: Project, editor: Editor, file: PsiFile): IntentionPreviewInfo {
        // Choose random language for the preview, as we cannot show a popup
        val language = injectableLanguages().firstOrNull() ?: return IntentionPreviewInfo.EMPTY
        inject(file, editor, language)
        return IntentionPreviewInfo.DIFF
    }

    private fun inject(
        file: PsiFile,
        editor: Editor,
        language: Injectable
    ) {
        val element = file.findElementAt(editor.caretModel.offset)
        val comment = MutableMagicComment<String, String>().apply { addValue(DefaultMagicKeys.INJECT_LANGUAGE, language.id) }
        element?.firstParentOfType(LatexEnvironment::class)?.addMagicComment(comment)
    }

    private fun chooseLanguage(editor: Editor, onChosen: (language: Injectable) -> Unit) {
        // Dummy to determine height of single cell
        val dimension = JLabel(LatexLanguage.displayName, EmptyIcon.ICON_16, SwingConstants.LEFT).minimumSize
        dimension.height *= 4

        val list = JBList(injectableLanguages()).apply {
            cellRenderer = object : ColoredListCellRenderer<Injectable>() {
                override fun customizeCellRenderer(list: JList<out Injectable>, language: Injectable, index: Int, selected: Boolean, hasFixed: Boolean) {
                    icon = language.icon
                    append(language.displayName)
                }
            }

            minimumSize = dimension
        }

        val popup = PopupChooserBuilder(list).setItemChoosenCallback { onChosen(list.selectedValue ?: return@setItemChoosenCallback) }
            .setFilteringEnabled { language -> (language as Injectable).displayName }
            .setMinSize(dimension)
            .createPopup()

        popup.showInBestPositionFor(editor)
    }

    private fun injectableLanguages(): List<Injectable> = Language.getRegisteredLanguages()
        // A parser definition is required for injection
        .filter { LanguageParserDefinitions.INSTANCE.forLanguage(it) != null }
        .map { Injectable.fromLanguage(it) }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.codeInsight.hint.HintManager
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import nl.hannahsten.texifyidea.util.containsKeyOrValue
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.findKeys
import nl.hannahsten.texifyidea.util.parser.inMathContext

/**
 * @author Hannah Schellekens
 */
open class LatexLeftRightParenthesesIntention : TexifyIntentionBase("Change to \\left..\\right") {

    companion object {

        private val brackets = mapOf(
            "(" to ")",
            "[" to "]",
            "<" to ">"
        )
    }

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (editor == null || file == null || !file.isLatexFile()) {
            return false
        }

        val element = file.findElementAt(editor.caretModel.offset - 1) ?: return false
        if (!element.inMathContext() || element is PsiComment) {
            return false
        }

        val (lookbehind, lookahead) = scan(editor, file) ?: return false
        return brackets.containsKeyOrValue(lookbehind) || brackets.containsKeyOrValue(lookahead)
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null || !file.isLatexFile()) {
            return
        }

        val caret = editor.caretModel
        val (lookbehind, lookahead) = scan(editor, file) ?: return

        if (brackets.containsKey(lookahead)) {
            replaceForward(editor, caret.offset, file)
        }
        else if (brackets.containsKey(lookbehind)) {
            replaceForward(editor, caret.offset - 1, file)
        }
        else if (brackets.containsValue(lookbehind)) {
            replaceBackward(editor, caret.offset - 1, file)
        }
        else if (brackets.containsValue(lookahead)) {
            replaceBackward(editor, caret.offset, file)
        }
    }

    private fun replaceForward(editor: Editor, offset: Int, file: PsiFile) {
        val document = editor.document
        val open = document.getText(TextRange.from(offset, 1))
        val close = brackets[open]

        // Scan document.
        var current = offset
        var nested = 0
        var closeOffset: Int? = null
        while (++current < file.textLength) {
            val char = document.getText(TextRange.from(current, 1))

            // Ignore comments.
            val element = file.findElementAt(current)
            if (element is PsiComment) {
                continue
            }

            if (!element!!.inMathContext() && element !is PsiWhiteSpace) {
                break
            }

            // Open nesting
            if (char == open) {
                nested++
                continue
            }

            // Close nesting
            if (char == close && nested > 0) {
                nested--
                continue
            }

            // Whenever met at correct closure
            if (char == close) {
                closeOffset = current
                break
            }
        }

        if (closeOffset == null) {
            HintManager.getInstance().showErrorHint(editor, "Could not find matching close '$close'")
            return
        }

        // Replace stuff
        runWriteAction {
            document.replaceString(closeOffset, closeOffset + 1, "\\right$close")
            document.replaceString(offset, offset + 1, "\\left$open")
            editor.caretModel.moveToOffset(offset + 6)
        }
    }

    private fun replaceBackward(editor: Editor, offset: Int, file: PsiFile) {
        val document = editor.document
        val close = document.getText(TextRange.from(offset, 1))
        val open = brackets.findKeys(close).first()

        // Scan document.
        var current = offset
        var nested = 0
        var openOffset: Int? = null
        while (--current < file.textLength) {
            val char = document.getText(TextRange.from(current, 1))

            // Ignore comments.
            val element = file.findElementAt(current)
            if (element is PsiComment) {
                continue
            }

            if (!element!!.inMathContext()) {
                break
            }

            // Open nesting
            if (char == close) {
                nested++
                continue
            }

            // Close nesting
            if (char == open && nested > 0) {
                nested--
                continue
            }

            // Whenever met at correct closure
            if (char == open) {
                openOffset = current
                break
            }
        }

        if (openOffset == null) {
            HintManager.getInstance().showErrorHint(editor, "Could not find matching open '$close'")
            return
        }

        // Replace stuff
        runWriteAction {
            document.replaceString(offset, offset + 1, "\\right$close")
            document.replaceString(openOffset, openOffset + 1, "\\left$open")
        }
    }

    private fun scan(editor: Editor, file: PsiFile): Pair<String, String>? {
        val caret = editor.caretModel
        val document = file.document() ?: return null

        // Check document ranges.
        if (caret.offset >= document.textLength) return null
        if (caret.offset - 1 < 0) return null

        val lookbehind = document.getText(TextRange.from(caret.offset - 1, 1))
        val lookahead = document.getText(TextRange.from(caret.offset, 1))

        return Pair(lookbehind, lookahead)
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.editor.MathEnvironmentEditor
import nl.hannahsten.texifyidea.lang.DefaultEnvironment
import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexDisplayMath
import nl.hannahsten.texifyidea.psi.LatexInlineMath
import nl.hannahsten.texifyidea.ui.PopupChooserCellRenderer
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.parser.*

/**
 * @author Hannah Schellekens, Abby Berkers
 */
open class LatexMathToggleIntention : TexifyIntentionBase("Convert to other math environment") {

    /**
     * Checks if the intention is available, i.e., it is inline/display math, or one of the other math environments.
     */
    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (editor == null || file == null || !file.isLatexFile()) {
            return false
        }

        val element = file.findElementAt(editor.caretModel.offset)
            ?: return false
        return element.inMathContext() || element.hasParent(LatexInlineMath::class)
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null || !file.isLatexFile()) {
            return
        }

        var element = file.findElementAt(editor.caretModel.offset) ?: return
        // Get the environment and its name of the (outer) math environment.
        val environmentName = when {
            element.hasParent(LatexInlineMath::class) -> {
                element = element.parentOfType(LatexInlineMath::class) ?: return
                "inline"
            }
            element.hasParent(LatexDisplayMath::class) -> {
                element = element.parentOfType(LatexDisplayMath::class)
                    ?: return
                "display"
            }
            else -> {
                element = element.findOuterMathEnvironment() ?: return
                element.childrenOfType<LatexBeginCommand>().first().environmentName()
            }
        } ?: return

        val availableEnvironments: List<String> = arrayOf(
            DefaultEnvironment.values()
                .filter { it.context == Environment.Context.MATH }
                .map { it.environmentName }
                .toTypedArray(),
            // Add the inline and display environments.
            arrayOf("inline", "display")
        )
            .flatten()
            // Remove equation*/displaymath, split/cases, and current environments.
            .filter { it != "split" && it != "cases" && it != "equation*" && it != "displaymath" && it != environmentName }
            .sorted()

        // Ask for the new environment name.
        JBPopupFactory.getInstance()
            .createPopupChooserBuilder(availableEnvironments)
            .setTitle("Math Environments")
            .setItemChosenCallback {
                // Apply the chosen environment.
                MathEnvironmentEditor(environmentName, it, editor, element).apply()
            }
            .setRenderer(PopupChooserCellRenderer())
            .createPopup().showInBestPositionFor(editor)
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.latex.codestyle.LatexTooLargeSectionInspection.Companion.findNextSection
import nl.hannahsten.texifyidea.inspections.latex.codestyle.LatexTooLargeSectionInspection.InspectionFix.Companion.findLabel
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.ui.CreateFileDialog
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.files.writeToFileUndoable
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.parser.findIndentation
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import java.io.File

/**
 * @author Hannah Schellekens
 */
open class LatexMoveSectionToFileIntention : TexifyIntentionBase("Move section contents to separate file") {

    companion object {

        private val affectedCommands = setOf("\\section", "\\chapter")
    }

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (editor == null || file == null || !file.isLatexFile()) {
            return false
        }

        val element = file.findElementAt(editor.caretModel.offset) ?: return false
        val selected = element as? LatexCommands ?: element.parentOfType(LatexCommands::class) ?: return false
        return selected.name in affectedCommands
    }

    // Focusing new dialogs when in write action throws an exception.
    override fun startInWriteAction() = false

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null) {
            return
        }

        // Find related elements.
        val element = file.findElementAt(editor.caretModel.offset) ?: return
        val sectionCommand = element as? LatexCommands ?: element.parentOfType(LatexCommands::class) ?: return
        val document = file.document() ?: return
        val label = findLabel(sectionCommand)
        val nextCmd = findNextSection(sectionCommand)

        // Find text.
        val start = label?.endOffset() ?: sectionCommand.endOffset()
        val cmdIndent = document.lineIndentation(document.getLineNumber(nextCmd?.textOffset ?: 0))
        val end = (nextCmd?.textOffset ?: document.textLength) - cmdIndent.length
        val text = document.getText(TextRange(start, end)).trimEnd().removeIndents()

        // Create new file.
        val fileNameBraces = sectionCommand.requiredParameter(0) ?: return
        // Remove the braces of the LaTeX command before creating a filename of it.
        val fileName = fileNameBraces.removeAll("{", "}")
            .formatAsFileName()
        val root = file.findRootFile().containingDirectory?.virtualFile?.canonicalPath ?: return

        // Display a dialog to ask for the location and name of the new file.
        val filePath = if (project.isTestProject().not()) {
            CreateFileDialog(file.containingDirectory?.virtualFile?.canonicalPath, fileName.formatAsFileName())
                .newFileFullPath ?: return
        }
        else file.containingDirectory?.virtualFile?.canonicalPath + File.separator + fileName

        // Execute write actions.
        runWriteAction {
            CommandProcessor.getInstance().executeCommand(project, {
                val createdFile = writeToFileUndoable(project, filePath, text, root)

                document.deleteString(start, end)
                val indent = sectionCommand.findIndentation()
                document.insertString(start, "\n$indent\\input{${createdFile.dropLast(4)}}\n\n")
            }, "Move Section to File", "Texify", UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION)
        }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.ui.CreateFileDialog
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.files.writeToFileUndoable
import nl.hannahsten.texifyidea.util.removeIndents
import org.intellij.lang.annotations.Language
import java.io.File

/**
 * @author Hannah Schellekens
 */
open class LatexMoveSelectionToFileIntention : TexifyIntentionBase("Move selection contents to separate file") {

    companion object {

        private const val minimumSelectionLength = 24
    }

    override fun startInWriteAction() = false

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (editor == null || file == null || !file.isLatexFile()) {
            return false
        }

        val selectionSize = selectionOffsets(editor).sumOf { (start, end): Pair<Int, Int> -> end - start }
        return selectionSize >= minimumSelectionLength
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null) {
            return
        }

        // Ask the user for a new file name.
        val offsets = selectionOffsets(editor)
        val document = editor.document

        // Display a dialog to ask for the location and name of the new file.
        val filePath = CreateFileDialog(file.containingDirectory?.virtualFile?.canonicalPath, "")
            .newFileFullPath ?: return

        // Find text.
        val text = StringBuilder()
        for ((start, end) in offsets) {
            val selected = document.getText(TextRange(start, end)).trimEnd().removeIndents()
            text.append(selected)
        }

        // Manage paths/file names.
        // Note that we do not override the user-specified filename to be LaTeX-like.
        // Path of virtual file always contains '/' as file separators.
        @Language("RegExp")
        val root = file.findRootFile().containingDirectory?.virtualFile?.canonicalPath ?: return

        // Execute write actions.
        runWriteAction {
            val createdFile = File(writeToFileUndoable(project, filePath, text.toString(), root))

            for ((start, end) in offsets.reversed()) {
                document.deleteString(start, end)
            }

            // The path of the created file contains the system's file separators, whereas the path of the root
            // (virtual file) always contains '/' as file separators.
            val fileNameRelativeToRoot = createdFile.absolutePath
                .replace(File.separator, "/")
                .replace("$root/", "")
            document.insertString(offsets.first().first, "\\input{${fileNameRelativeToRoot.dropLast(4)}}")
        }
    }

    private fun selectionOffsets(editor: Editor): List<Pair<Int, Int>> {
        return editor.caretModel.allCarets
            .map { Pair(it.selectionStart, it.selectionEnd) }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.settings.TexifySettings
import kotlin.math.max

/**
 * @author Hannah Schellekens
 */
open class LatexToggleSmartQuotesIntention : TexifyIntentionBase("Toggle smart quotes") {

    companion object {

        private val triggerCharacters = setOf('\'', '"', '`')
    }

    /**
     * Contains the preferred setting of the user if they have already selected one.
     */
    private var selectedSmartQuoteSetting = when (val replacement = TexifySettings.getInstance().automaticQuoteReplacement) {
        TexifySettings.QuoteReplacement.NONE -> TexifySettings.QuoteReplacement.LIGATURES
        else -> replacement
    }

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        val caret = editor?.caretModel ?: return false
        val element = file?.findElementAt(caret.offset) ?: return false

        // Also check the element 1 position back, because the following line:
        // ``kameel''
        // with the cursor at the end will result in the following whitespace (therefore, no quotes).
        val elementBefore = file.findElementAt(max(0, caret.offset - 1)) ?: return false

        return triggerCharacters.any { char ->
            element.textContains(char) || elementBefore.textContains(char)
        }
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        val settings = TexifySettings.getInstance()
        val result = when (settings.automaticQuoteReplacement) {
            TexifySettings.QuoteReplacement.NONE -> selectedSmartQuoteSetting
            else -> TexifySettings.QuoteReplacement.NONE
        }

        settings.automaticQuoteReplacement = result
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexRequiredParam
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * @author Hannah Schellekens
 */
open class LatexUnpackUsepackageIntention : TexifyIntentionBase("Split into multiple \\usepackage commands") {

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (editor == null || file == null || !file.isLatexFile()) {
            return false
        }

        val selected = file.findElementAt(editor.caretModel.offset) ?: return false
        val command = selected.parentOfType(LatexCommands::class) ?: return false

        if (command.name != "\\usepackage") {
            return false
        }

        val required = command.requiredParameter(0)
        return required != null && required.contains(",")
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null || !file.isLatexFile()) {
            return
        }

        // Find packages.
        val selected = file.findElementAt(editor.caretModel.offset) ?: return
        val command = selected.parentOfType(LatexCommands::class) ?: return
        val required = command.firstChildOfType(LatexRequiredParam::class) ?: return
        val requiredText = required.text.trimRange(1, 1)
        val packages = PatternMagic.parameterSplit.split(requiredText)

        // Reorganise includes.
        val document = file.document() ?: return
        val offset = command.textOffset

        runWriteAction {
            document.deleteString(offset, command.endOffset())
            for (i in packages.size - 1 downTo 0) {
                val newline = if (i > 0) "\n" else ""
                val pack = packages[i]
                document.insertString(offset, "$newline\\usepackage{$pack}")
            }
        }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.ui.PopupChooserCellRenderer
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.getAllRequiredArguments
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.name

class LatexVerbatimToggleIntention : TexifyIntentionBase("Convert to other verbatim command or environment") {

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (editor == null || file == null || !file.isLatexFile()) {
            return false
        }

        val element = file.findElementAt(editor.caretModel.offset) ?: return false

        return element.isVerbCommand() || element.isVerbEnvironment()
    }

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null || !file.isLatexFile()) {
            return
        }

        val element = file.findElementAt(editor.caretModel.offset) ?: return

        val availableEnvironments: List<String> = (CommandMagic.verbatim.keys + EnvironmentMagic.verbatim)
            .filter { it != element.getName() }

        if (availableEnvironments.size == 1) {
            replaceVerbatim(element, availableEnvironments.firstOrNull()!!, file, project)
        }
        // Ask for the new environment name when there are multiple options.
        else {
            JBPopupFactory.getInstance()
                .createPopupChooserBuilder(availableEnvironments)
                .setTitle("Verbatim Environments")
                .setItemChosenCallback { replaceVerbatim(element, it, file, project) }
                .setRenderer(PopupChooserCellRenderer())
                .createPopup()
                .showInBestPositionFor(editor)
        }
    }

    private fun PsiElement.isVerbCommand() = getName() in CommandMagic.verbatim

    private fun PsiElement.isVerbEnvironment() = getName() in EnvironmentMagic.verbatim

    /**
     * Get the name of the command or environment.
     */
    private fun PsiElement.getName(): String? =
        firstParentOfType(LatexCommands::class)?.name?.removePrefix("\\")
            ?: firstParentOfType(LatexEnvironment::class)?.name()?.name

    /**
     * Replace the psi element of the old verbatim with a psi element with the new verbatim.
     */
    private fun replaceVerbatim(oldVerbatim: PsiElement, newVerbatim: String, file: PsiFile, project: Project) {
        val (content, parent, commandArgCharacter) = findVerbatimInformation(oldVerbatim)

        val newElement = constructNewVerbatim(newVerbatim, content, commandArgCharacter, project) ?: return

        runWriteCommandAction(project) {
            parent?.node?.replaceChild(parent.firstChild.node, newElement.node)

            // When the old verbatim was a command (and thus inline) and the new verbatim is an environment,
            // add a newline before the environment.
            if (oldVerbatim.isVerbCommand() && newVerbatim in EnvironmentMagic.verbatim) {
                addNewLines(parent, newElement, project)
            }

            // When the old verbatim was an environment and the new verbatim is a command, replace the new lines that
            // surrounded the environment with single spaces.
            if (oldVerbatim.isVerbEnvironment() && newVerbatim in CommandMagic.verbatim) {
                removeNewLines(parent, project)
            }

            // Check if the newly inserted verbatim depends on a package and insert the package when needed.
            findDependency(newElement)?.let { file.insertUsepackage(it) }
        }
    }

    /**
     * Use the index to find if the `verbatim` element we insert depends on a package.
     */
    private fun findDependency(verbatim: PsiElement): LatexPackage? =
        (verbatim as? LatexCommands)?.let {
            LatexCommand.lookup(it)?.firstOrNull()?.dependency
        } ?: verbatim.getName()?.let {
            Environment.lookup(it)?.dependency
        }

    /**
     * Get all information about [oldVerbatim] that is needed to replace it with a new verbatim.
     *
     * Returns a triple with
     * - `content: String` The text inside the command or environment. This has to be included in the new verbatim.
     * - `parent: PsiElement` The parent psi element. The child of this parent will be replaced with the new verbatim.
     * - `Boolean` Does the old verbatim command use characters other than `{}` to enclose its argument?
     *   (e.g. `\verb|test|` would set this boolean to true).
     */
    private fun findVerbatimInformation(oldVerbatim: PsiElement): Triple<String?, PsiElement?, Boolean> {
        return if (oldVerbatim.isVerbCommand()) {
            val content = oldVerbatim.firstParentOfType(LatexCommands::class)?.getAllRequiredArguments()?.firstOrNull()
            val parent = oldVerbatim.firstParentOfType(LatexCommands::class)?.parent
            Triple(content, parent, CommandMagic.verbatim[oldVerbatim.getName()] == true)
        }
        else if (oldVerbatim.isVerbEnvironment()) {
            val content = oldVerbatim.firstParentOfType(LatexEnvironment::class)?.environmentContent?.text
            val parent = oldVerbatim.firstParentOfType(LatexEnvironment::class)?.parent
            Triple(content, parent, false)
        }
        else Triple(null, null, false)
    }

    /**
     * Use [LatexPsiHelper] to create a new psi element.
     */
    private fun constructNewVerbatim(
        newVerbatim: String,
        content: String?,
        commandArgCharacter: Boolean,
        project: Project
    ): PsiElement? =
        if (newVerbatim in CommandMagic.verbatim.keys) {
            val newArgCharacter = CommandMagic.verbatim[newVerbatim] == true
            val arg = when {
                commandArgCharacter && newArgCharacter -> content
                commandArgCharacter && !newArgCharacter -> "{${content?.drop(1)?.dropLast(1)}}"
                !commandArgCharacter && newArgCharacter -> "|$content|"
                else -> "{$content}"
            }
            LatexPsiHelper(project).createFromText("\\$newVerbatim$arg")
                .firstChildOfType(LatexCommands::class)
        }
        else {
            val environmentContent = if (commandArgCharacter) content?.drop(1)?.dropLast(1) else content
            LatexPsiHelper(project).createFromText("\\begin{$newVerbatim}\n$environmentContent\n\\end{$newVerbatim}")
                .firstChildOfType(LatexEnvironment::class)
        }

    /**
     * Add a new line before and after the new environment when the current white space there is not a white space.
     */
    private fun addNewLines(parent: PsiElement?, newElement: PsiElement, project: Project) {
        if (parent?.prevSibling is PsiWhiteSpace && !parent.prevSibling.textContains('\n')) {
            LatexPsiHelper(project).createSpacing("\n")?.node?.let {
                parent.node?.addChild(it, newElement.node)
            }
        }

        if (parent?.nextSibling is PsiWhiteSpace && !parent.nextSibling.textContains('\n')) {
            LatexPsiHelper(project).createSpacing("\n")?.node?.let {
                parent.node?.addChild(it, null)
            }
        }
    }

    /**
     * Replace the new lines before and after [parent] with single spaces.
     */
    private fun removeNewLines(parent: PsiElement?, project: Project) {
        if (parent?.prevSibling is PsiWhiteSpace && parent.prevSibling.textContains('\n')) {
            parent.prevSibling.node.let { newLine ->
                LatexPsiHelper(project).createSpacing()?.node?.let {
                    newLine.psi.parent.node.replaceChild(newLine, it)
                }
            }
        }

        if (parent?.nextSibling is PsiWhiteSpace && parent.nextSibling.textContains('\n')) {
            parent.nextSibling.node.let { newLine ->
                LatexPsiHelper(project).createSpacing()?.node?.let {
                    newLine.psi.parent.node.replaceChild(newLine, it)
                }
            }
        }
    }
}

package nl.hannahsten.texifyidea.intentions

import com.intellij.codeInsight.intention.IntentionAction
import nl.hannahsten.texifyidea.inspections.InsightGroup

/**
 * @author Hannah Schellekens
 */
abstract class TexifyIntentionBase(

    /**
     * The name of the intention that shows up in the intention overview.
     */
    val name: String,

    /**
     * The group to which the intention belongs (duh).
     */
    val insightGroup: InsightGroup = InsightGroup.LATEX

) : IntentionAction {

    override fun startInWriteAction() = false

    override fun getText() = name

    override fun getFamilyName() = name
}

package nl.hannahsten.texifyidea.lang

import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.BIBLATEX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.DEFAULT

/**
 * All known entry type fields for bibtex and biblatex.
 *
 * @author Hannah Schellekens
 */
@Suppress("unused") // All are used in completion
enum class BibtexDefaultEntryField(
    override val fieldName: String,
    override val description: String,
    override val dependency: LatexPackage = DEFAULT
) : BibtexEntryField {

    ADDRESS("address", "Publisher's address (usually just the city, but can be the full address for lesser-known publishers)"),
    ANNOTE("annote", "An annotation for annotated bibliography styles (not typical)"),
    AUTHOR("author", "The name(s) of the author(s) (in the case of more than one author, separated by and)"),
    BOOKTITLE("booktitle", "The title of the book, if only part of it is being cited"),
    CHAPTER("chapter", "The chapter number"),
    CROSSREF("crossref", "The key of the cross-referenced entry"),
    EDITION("edition", "The edition of a book, long form (such as \"First\" or \"Second\")"),
    EDITOR("editor", "The name(s) of the editor(s)"),
    HOWPUBLISHED("howpublished", "How it was published, if the publishing method is nonstandard"),
    INSTITUTION("institution", "The institution that was involved in the publishing, but not necessarily the publisher"),
    JOURNAL("journal", "The journal or magazine the work was published in"),
    KEY("key", "A hidden field used for specifying or overriding the alphabetical order of entries (when the \"author\" and \"editor\" fields are missing). Note that this is very different from the key (mentioned just after this list) that is used to cite or cross-reference the entry."),
    MONTH("month", "The month of publication (or, if unpublished, the month of creation)"),
    NOTE("note", "Miscellaneous extra information"),
    NUMBER("number", "The \"(issue) number\" of a journal, magazine, or tech-report, if applicable. (Most publications have a \"volume\", but no \"number\" field.)"),
    ORGANISATION("organization", "The conference sponsor"),
    PAGES("pages", "Page numbers, separated either by commas or double-hyphens (en dash)."),
    PUBLISHER("publisher", "The publisher's name"),
    SCHOOL("school", "The school where the thesis was written"),
    SERIES("series", "The series of books the book was published in (e.g. \"The Hardy Boys\" or \"Lecture Notes in Computer Science\")"),
    TITLE("title", "The title of the work"),
    TYPE("type", "The field overriding the default type of publication (e.g. \"Research Note\" for techreport, \"{PhD} dissertation\" for phdthesis, \"Section\" for inbook/incollection)"),
    VOLUME("volume", "The volume of a journal or multi-volume book"),
    YEAR("year", "The year of publication (or, if unpublished, the year of creation)"),

    // BibLaTeX
    ABSTRACT("abstract", "This field is intended for recording abstracts in a bib file, to be printed by a special bibliography style. It is not used by all standard bibliography styles.", BIBLATEX),
    ADDENDUM("addendum", "Miscellaneous bibliographic data to be printed at the end of the entry. This is similar to the note field except that it is printed at the end of the bibliography entry.", BIBLATEX),
    AFTERWORD("afterword", "The author(s) of an afterword to the work.", BIBLATEX),
    ANNOTATION("annotation", "This field may be useful when implementing a style for annotated bibliographies.", BIBLATEX),
    ANNOTATOR("annotator", "The author(s) of annotations to the work.", BIBLATEX),
    AUTHORTYPE("authortype", "The type of author.", BIBLATEX),
    BOOKAUTHOR("bookauthor", "The author(s) of the booktitle.", BIBLATEX),
    BOOKPAGINATION("bookpagination", "If the work is published as part of another one, this is the pagination scheme of the en-closing work, i. e., bookpagination relates to pagination like booktitle to title.", BIBLATEX),
    BOOKSUBTITLE("booksubtitle", "The subtitle related to the booktitle", BIBLATEX),
    BOOKTITLEADDON("booktitleaddon", "An annex to the booktitle.", BIBLATEX),
    COMMENTATOR("commentator", "The author(s) of a commentary to the work", BIBLATEX),
    DATE("date", "The publication date.", BIBLATEX),
    DOI("doi", "The Digital Object Identifier of the work.", BIBLATEX),
    EDITORA("editora", "A secondary editor performing a different editorial role", BIBLATEX),
    EDITORB("editorb", "A secondary editor performing a different editorial role", BIBLATEX),
    EDITORC("editorc", "A secondary editor performing a different editorial role", BIBLATEX),
    EDITORTYPE("editortype", "The type of editorial role performed by the editor.", BIBLATEX),
    EDITORATYPE("editoratype", "Similar to editortype but for editor a.", BIBLATEX),
    EDITORBTYPE("editorbtype", "Similar to editortype but for editor a.", BIBLATEX),
    EDITORCTYPE("editorctype", "Similar to editortype but for editor a.", BIBLATEX),
    EID("eid", "The electronic identifier of an @article", BIBLATEX),
    ENTRYSUBTYPE("entrysubtype", "This field may be used to specify a subtype of an entry type.", BIBLATEX),
    EPRINT("eprint", "The electronic identifier of an online publication.", BIBLATEX),
    EPRINTCLASS("eprintclass", "Additional information related to the resource indicated by the eprinttype field", BIBLATEX),
    EPRINTTYPE("eprinttype", "The type of the eprint identifier.", BIBLATEX),
    EVENTDATE("eventdate", "The date of a conference, symposium, or some other event in @proceedings and @inproceedings entries.", BIBLATEX),
    EVENTTITLE("eventtitle", "The title of a conference, symposium, or some other event in @proceedings and @inproceedings entries.", BIBLATEX),
    EVENTTITLEADDON("eventtitleaddon", "An annex to the eventtitle field.", BIBLATEX),
    FILE("file", "A local link to a pdf or other version of the work.", BIBLATEX),
    FOREWORD("foreword", "The author(s) of a foreword to the work.", BIBLATEX),
    HOLDER("holder", "The holder(s) of @patent, if different from the author.", BIBLATEX),
    INDEXTITLE("indextitle", "A title to use for indexing instead of the regular title field.", BIBLATEX),
    INTRODUCTION("introduction", "The author(s) of an introduction to the work.", BIBLATEX),
    ISAN("isan", "The International Standard Audiovisual Number of an audiovisual work.", BIBLATEX),
    ISBN("isbn", "The International Standard Book Number of a book.", BIBLATEX),
    ISMN("ismn", "The International Standard Music Number for printed music such as musical scores.", BIBLATEX),
    ISRN("isrn", "The International Standard Technical Report Number of a technical report.", BIBLATEX),
    ISSN("issn", "The International Standard Serial Number of a periodical.", BIBLATEX),
    ISSUE("issue", "The issue of a journal", BIBLATEX),
    ISSUESUBTITLE("issuesubtitle", "The subtitle of a specifi issue of a journal or other periodical.", BIBLATEX),
    ISSUETITLE("issuetitle", "The title of a specific issue of a journal or other periodial.", BIBLATEX),
    ISWC("iswc", "The International Standard Work Code of musical work.", BIBLATEX),
    JOURNALSUBTITLE("journalsubtitle", "The subtitle of a journal, a newspaper, or some other periodical.", BIBLATEX),
    JOURNALTITLE("journaltitle", "The name of a journal, a newspaper, or some other periodical.", BIBLATEX),
    LABEL("label", "A designation to be used by the citation style as a substitute for the regular label if any data required to generate the regular label is missing.", BIBLATEX),
    LANGUAGE("language", "The language(s) of the work.", BIBLATEX),
    LIBRARY("library", "This field may be useful to record information such as a library name and a call number.", BIBLATEX),
    LOCATION("location", "The place(s) of publication.", BIBLATEX),
    MAINSUBTITLE("mainsubtitle", "The subtitle related to the maintitle.", BIBLATEX),
    MAINTITLE("maintitle", "The main title of a multi-volume book, such as Collected Works.", BIBLATEX),
    MAINTITLEADDON("maintitleaddon", "An annex to the maintitle.", BIBLATEX),
    NAMEADDON("nameaddon", "An addon to be printed immediately after the author name in the bibliography.", BIBLATEX),
    ORIGLANGUAGE("origlanguage", "If the work is a translation, the language(s) of the original work.", BIBLATEX),
    ORIGLOCATION("origlocation", "If the work is a translation, a reprint, or something similar, the location of the original edition.", BIBLATEX),
    ORIGPUBLISHER("origpublisher", "If the work is a translation, a reprint, or something similar, the publisher of the original edition.", BIBLATEX),
    ORIGTITLE("origtitle", "If the work is a translation, the title of the original work.", BIBLATEX),
    PAGETOTAL("pagetotal", "The total number of pages of the work.", BIBLATEX),
    PAGINATION("pagination", "The pagination of the work.", BIBLATEX),
    PART("part", "The number of a partial volume.", BIBLATEX),
    PUBSTATE("pubstate", "The publication state of the work, e.g., in press.", BIBLATEX),
    REPRINTTITLE("reprinttitle", "The title of a reprint of the work.", BIBLATEX),
    SHORTAUTHOR("shortauthor", "The author(s) of the work, given in an abbreviated form.", BIBLATEX),
    SHORTEDITOR("shorteditor", "The editor(s) of the work, given in an abbreviated form.", BIBLATEX),
    SHORTHAND("shorthand", "A special designation to be used by the citation style instead of the usual label.", BIBLATEX),
    SHORTHANDINTRO("shorthandintro", "The verbose citation styles which comes with this package use a phrase like “hence-forth cited as [shorthand]” to introduce shorthands on the first citation.", BIBLATEX),
    SHORTJOURNAL("shortjournal", "A short version or an acronym of the journaltitle.", BIBLATEX),
    SHORTSERIERS("shortseries", "A short version or an acronym of the series field.", BIBLATEX),
    SHORTTITLE("shorttitle", "The title in an abridged form.", BIBLATEX),
    SUBTITLE("subtitle", "The subtitle of the work.", BIBLATEX),
    TITLEADDON("titleaddon", "An annex to the title.", BIBLATEX),
    TRANSLATOR("translator", "The translator(s) of the title or booktitle, depending on the entry type.", BIBLATEX),
    URL("url", "The url of an online publication. If it is not URL-escaped (no '%' chars) it will be URI-escaped according to RFC 3987, that is, even Unicode chars will be correctly escaped.", BIBLATEX),
    URLDATE("urldate", "The access date of the addres specified in the url field.", BIBLATEX),
    VENUE("venue", "The location of a conference, a symposium, or some other event in @proceedings and @inproceedings entries.", BIBLATEX),
    VERSION("version", "The revision number of a piece of software, a manual, etc.", BIBLATEX),
    VOLUMES("volumes", "The total number of volumes of a multi-volume work.", BIBLATEX),

    // Special BibLaTeX fields
    ENTRYSET("entryset", "This field is specific to entry sets.", BIBLATEX),
    EXECUTE("execute", "A special field which holds arbitrary TeX code to be executed whenever the data of the respective entry is accessed.", BIBLATEX),
    GENDER("gender", "The gender of the author or the gender of the editor if there is no author.", BIBLATEX),
    LANGID("langid", "The language id of the bib entry.", BIBLATEX),
    LANGIDOPTS("langidopts", "For polyglossia users, allows per-entry language specific options.", BIBLATEX),
    IDS("ids", "Citation key aliases for the main citation key.", BIBLATEX),
    INDEXSORTTITLE("indexsorttitle", "The title used when sorting the index.", BIBLATEX),
    KEYWORDS("keywords", "A separated list of keywords.", BIBLATEX),
    OPTIONS("options", "A separated list of options in <key>=<value> notation.", BIBLATEX),
    PRESORT("presort", "A special field used to modify the sorting order of the bibliography.", BIBLATEX),
    RELATED("related", "Citation keys of other entries which have a relationship to this entry.", BIBLATEX),
    RELATEDOPTIONS("relatedoptions", "Per-type options to set for a related entry.", BIBLATEX),
    RELATEDTYPE("relatedtype", "An identifier which specified the type of relationships for the keys listed in the related field.", BIBLATEX),
    RELATEDSTRING("relatedstring", "A field used to override the bib string specified in relatedtype.", BIBLATEX),
    SORTKEY("sortkey", "A field used to modify the sorting order of the bibliography.", BIBLATEX),
    SORTNAME("sortname", "A name or list of names used to modify the sorting order of the bibliography.", BIBLATEX),
    SORTSHORTHAND("sortshorthand", "Similar to sortkey but used in the list of shorthands.", BIBLATEX),
    SORTTITLE("sorttitle", "A field used to modify the sorting order of the bibliography.", BIBLATEX),
    SORTYEAR("sortyear", "A field used to modify the sorting order of the bibliography.", BIBLATEX),
    XDATA("xdata", "This field inherits data from one or more @xdata entries.", BIBLATEX),
    XREF("xref", "This field is an alternative cross-referencing mechanism.", BIBLATEX);
}

package nl.hannahsten.texifyidea.lang

import nl.hannahsten.texifyidea.lang.BibtexDefaultEntryField.*
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.BIBLATEX
import java.util.*
import kotlin.collections.HashMap

/**
 * All entry types in bibtex and biblatex.
 *
 * @author Hannah Schellekens
 */
enum class BibtexDefaultEntryType(
    override val token: String,
    override val description: String,
    override val required: Array<BibtexEntryField>,
    override val optional: Array<BibtexEntryField>,
    override val dependency: LatexPackage = LatexPackage.DEFAULT
) : BibtexEntryType {

    // Regular entry types.
    ARTICLE(
        "article",
        "An article from a journal or magazine.",
        arrayOf(AUTHOR, TITLE, JOURNAL, YEAR),
        arrayOf(NUMBER, PAGES, MONTH, NOTE, VOLUME, KEY)
    ),
    BOOK(
        "book",
        "A book with an explicit publisher.",
        arrayOf(AUTHOR, TITLE, PUBLISHER, YEAR),
        arrayOf(EDITION, VOLUME, NUMBER, SERIES, ADDRESS, EDITION, MONTH, NOTE, KEY)
    ),
    BOOKLET(
        "booklet",
        "A work that is printed and bound, but without a named publisher or sponsoring institution.",
        arrayOf(TITLE),
        arrayOf(AUTHOR, HOWPUBLISHED, ADDRESS, MONTH, YEAR, NOTE, KEY)
    ),
    CONFERENCE(
        "conference",
        "The same as inproceedings, included for Scribe compatibility.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, YEAR),
        arrayOf(EDITOR, VOLUME, NUMBER, SERIES, PAGES, ADDRESS, MONTH, ORGANISATION, PUBLISHER, NOTE, KEY)
    ),
    INBOOK(
        "inbook",
        "A part of a book, usually untitled. May be a chapter (or section, etc.) and/or a range of pages.",
        arrayOf(AUTHOR, TITLE, PAGES, PUBLISHER, YEAR),
        arrayOf(EDITOR, CHAPTER, VOLUME, NUMBER, SERIES, TYPE, ADDRESS, EDITION, MONTH, NOTE, KEY)
    ),
    INCOLLECTION(
        "incollection",
        "A part of a book having its own title.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, PUBLISHER, YEAR),
        arrayOf(EDITOR, VOLUME, NUMBER, SERIES, TYPE, CHAPTER, PAGES, ADDRESS, EDITION, MONTH, NOTE, KEY)
    ),
    INPROCEEDINGS(
        "inproceedings",
        "An article in a conference proceedings.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, YEAR),
        arrayOf(EDITOR, VOLUME, NUMBER, SERIES, PAGES, ADDRESS, MONTH, ORGANISATION, PUBLISHER, NOTE, KEY)
    ),
    MANUAL(
        "manual",
        "Technical documentation.",
        arrayOf(TITLE),
        arrayOf(AUTHOR, ORGANISATION, ADDRESS, EDITION, MONTH, YEAR, NOTE, KEY)
    ),
    MASTERTHESIS(
        "masterthesis",
        "A Master's thesis.",
        arrayOf(AUTHOR, TITLE, SCHOOL, YEAR),
        arrayOf(TYPE, ADDRESS, MONTH, NOTE, KEY)
    ),
    MISC(
        "misc",
        "For use when nothing else fits.",
        emptyArray(),
        arrayOf(AUTHOR, TITLE, HOWPUBLISHED, MONTH, YEAR, NOTE, KEY)
    ),
    PHDTHESIS(
        "phdthesis",
        "A Ph.D. thesis.",
        arrayOf(AUTHOR, TITLE, SCHOOL, YEAR),
        arrayOf(TYPE, ADDRESS, MONTH, NOTE, KEY)
    ),
    PROCEEDINGS(
        "proceedings",
        "The proceedings of a conference.",
        arrayOf(TITLE, YEAR),
        arrayOf(EDITOR, VOLUME, NUMBER, SERIES, ADDRESS, MONTH, PUBLISHER, ORGANISATION, NOTE, KEY)
    ),
    TECHREPORT(
        "techreport",
        "A report published by a school or other institution, usually numbered within a series.",
        arrayOf(AUTHOR, TITLE, INSTITUTION, YEAR),
        arrayOf(TYPE, NUMBER, ADDRESS, MONTH, NOTE, KEY)
    ),
    UNPUBLISHED(
        "unpublished",
        "A document having an author and title, but not formally published.",
        arrayOf(AUTHOR, TITLE, NOTE),
        arrayOf(MONTH, YEAR, KEY)
    ),

    // Special entries
    STRING(
        "string",
        "Define a string to be used later on in the bibliography",
        emptyArray(),
        emptyArray()
    ),
    PREAMBLE(
        "preamble",
        "You can define some LaTeX commands that will be included in the .bbl file generated by BibTex using the preamble",
        emptyArray(),
        emptyArray()
    ),

    // BibLaTeX entries. Source: biblatex docs
    BIBLATEX_ARTICLE(
        "article",
        "An article in a journal, magazine, newspaper, or other periodical which forms a self-contained unit with its own title.",
        arrayOf(AUTHOR, TITLE, JOURNALTITLE, DATE),
        arrayOf(TRANSLATOR, ANNOTATOR, COMMENTATOR, SUBTITLE, TITLEADDON, EDITOR, EDITORA, EDITORB, EDITORC, JOURNALSUBTITLE, ISSUETITLE, ISSUESUBTITLE, LANGUAGE, ORIGLANGUAGE, SERIES, VOLUME, NUMBER, EID, ISSUE, MONTH, PAGES, VERSION, NOTE, ISSN, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_BOOK(
        "book",
        "A single-volume book with one or more authors where the authors share credit for the work as a whole.",
        arrayOf(AUTHOR, TITLE, DATE),
        arrayOf(EDITOR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, LANGUAGE, ORIGLANGUAGE, VOLUME, PART, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    MVBOOK(
        "mvbook",
        "A multi-volume @book.",
        arrayOf(AUTHOR, TITLE, DATE),
        arrayOf(EDITOR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, LANGUAGE, ORIGLANGUAGE, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_INBOOK(
        "inbook",
        "A part of a book which forms a self-contained unit with its own title.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, DATE),
        arrayOf(BOOKAUTHOR, EDITOR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, BOOKSUBTITLE, BOOKTITLEADDON, LANGUAGE, ORIGLANGUAGE, VOLUME, PART, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BOOKINBOOK(
        "bookinbook",
        "This type is similar to @inbook but intended for works originally published as a\n" +
            "stand-alone book.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, DATE),
        arrayOf(BOOKAUTHOR, EDITOR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, BOOKSUBTITLE, BOOKTITLEADDON, LANGUAGE, ORIGLANGUAGE, VOLUME, PART, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    SUPPBOOK(
        "suppbook",
        "Supplemental material in a @book. This type is closely related to the @inbook entry type.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, DATE),
        arrayOf(BOOKAUTHOR, EDITOR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, BOOKSUBTITLE, BOOKTITLEADDON, LANGUAGE, ORIGLANGUAGE, VOLUME, PART, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_BOOKLET(
        "booklet",
        "A book-like work without a format publisher or sponsoring insitution.",
        arrayOf(AUTHOR, TITLE, DATE),
        arrayOf(EDITOR, SUBTITLE, TITLEADDON, LANGUAGE, HOWPUBLISHED, TYPE, NOTE, LOCATION, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    COLLECTION(
        "collection",
        "A single-volume collection with multiple, self-contained contributions by distinct authors which have their own title.",
        arrayOf(EDITOR, TITLE, DATE),
        arrayOf(EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, LANGUAGE, ORIGLANGUAGE, VOLUME, PART, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    MVCOLLECTION(
        "mvcollection",
        "A multi-volume @collection.",
        arrayOf(EDITOR, TITLE, DATE),
        arrayOf(YEAR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, LANGUAGE, ORIGLANGUAGE, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_INCOLLECTION(
        "incollection",
        "A contribution to a collection which forms a self-contained unit with a distinct author and title.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, DATE),
        arrayOf(YEAR, EDITOR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, BOOKSUBTITLE, BOOKTITLEADDON, LANGUAGE, ORIGLANGUAGE, VOLUME, PART, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_SUPPCOLLECTION(
        "suppcollection",
        "Supplemental material in a @collection. This type is similar to @suppbook but related to the @collection entry type. ",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, DATE),
        arrayOf(YEAR, EDITOR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, BOOKSUBTITLE, BOOKTITLEADDON, LANGUAGE, ORIGLANGUAGE, VOLUME, PART, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    DATASET(
        "dataset",
        "A data set or a similar collection of (mostly) raw data.",
        arrayOf(AUTHOR, TITLE, DATE),
        arrayOf(YEAR, SUBTITLE, TITLEADDON, LANGUAGE, EDITION, TYPE, SERIES, NUMBER, VERSION, NOTE, ORGANISATION, PUBLISHER, LOCATION, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_MANUAL(
        "manual",
        "Technical or other documentation.",
        arrayOf(TITLE, DATE),
        arrayOf(AUTHOR, EDITOR, YEAR, SUBTITLE, TITLEADDON, LANGUAGE, EDITION, TYPE, SERIES, NUMBER, VERSION, NOTE, ORGANISATION, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_MISC(
        "misc",
        "A fallback type for entries which do not fit into any other category.",
        arrayOf(TITLE, DATE),
        arrayOf(AUTHOR, EDITOR, YEAR, SUBTITLE, TITLEADDON, LANGUAGE, HOWPUBLISHED, TYPE, VERSION, NOTE, ORGANISATION, LOCATION, MONTH, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    ONLINE(
        "online",
        "A website.",
        arrayOf(AUTHOR, TITLE, DATE, URL),
        arrayOf(SUBTITLE, TITLEADDON, LANGUAGE, VERSION, NOTE, ORGANISATION, ADDENDUM, PUBSTATE, EPRINTCLASS, EPRINTTYPE, URLDATE),
        BIBLATEX
    ),
    PATENT(
        "patent",
        "A patent or patent request.",
        arrayOf(AUTHOR, TITLE, NUMBER, DATE),
        arrayOf(YEAR, HOLDER, SUBTITLE, TITLEADDON, TYPE, VERSION, LOCATION, NOTE, MONTH, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    PERIODICAL(
        "periodical",
        "A complete issue of a periodical.",
        arrayOf(EDITOR, TITLE, DATE),
        arrayOf(YEAR, EDITORA, EDITORB, EDITORC, SUBTITLE, ISSUETITLE, ISSUESUBTITLE, LANGUAGE, SERIES, VOLUME, NUMBER, ISSUE, MONTH, NOTE, ISSN, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    SUPPPERIODICAL(
        "suppperiodical",
        "Supplemental material in a @periodical. This type is similar to @suppbook but related to the @periodical entry type.",
        arrayOf(EDITOR, TITLE, DATE),
        arrayOf(YEAR, EDITORA, EDITORB, EDITORC, SUBTITLE, ISSUETITLE, ISSUESUBTITLE, LANGUAGE, SERIES, VOLUME, NUMBER, ISSUE, MONTH, NOTE, ISSN, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_PROCEEDINGS(
        "proceedings",
        "A single-volume conference proceedings.",
        arrayOf(TITLE, DATE),
        arrayOf(YEAR, EDITOR, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, EVENTTITLE, EVENTTITLEADDON, EVENTDATE, VENUE, LANGUAGE, VOLUME, PART, VOLUMES, SERIES, NUMBER, NOTE, ORGANISATION, PUBLISHER, LOCATION, MONTH, ISBN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    MVPROCEEDINGS(
        "mvproceedings",
        "A multi-volume @proceedings entry.",
        arrayOf(TITLE, DATE),
        arrayOf(YEAR, EDITOR, SUBTITLE, TITLEADDON, EVENTTITLE, EVENTTITLEADDON, EVENTDATE, VENUE, LANGUAGE, VOLUMES, SERIES, NUMBER, NOTE, ORGANISATION, PUBLISHER, LOCATION, MONTH, ISBN, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_INPROCEEDINGS(
        "inproceedings",
        "An article in a conference proceedings.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, DATE),
        arrayOf(EDITOR, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, BOOKSUBTITLE, BOOKTITLEADDON, EVENTTITLE, EVENTTITLEADDON, EVENTDATE, VENUE, LANGUAGE, VOLUME, PART, VOLUMES, SERIES, NUMBER, NOTE, ORGANISATION, PUBLISHER, LOCATION, MONTH, ISBN, CHAPTER, PAGES, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    REFERENCE(
        "reference",
        "A single-volume work of reference such as an encyclopedia or a dictionary. This is a\n" +
            "more specific variant of the generic @collection entry type",
        arrayOf(EDITOR, TITLE, DATE),
        arrayOf(EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, LANGUAGE, ORIGLANGUAGE, VOLUME, PART, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    MVREFERENCE(
        "mvreference",
        "A multi-volume @reference entry. ",
        arrayOf(EDITOR, TITLE, DATE),
        arrayOf(YEAR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, LANGUAGE, ORIGLANGUAGE, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    INREFERENCE(
        "inreference",
        "An article in a work of reference.",
        arrayOf(EDITOR, TITLE, DATE),
        arrayOf(YEAR, EDITORA, EDITORB, EDITORC, TRANSLATOR, ANNOTATOR, COMMENTATOR, INTRODUCTION, FOREWORD, AFTERWORD, SUBTITLE, TITLEADDON, LANGUAGE, ORIGLANGUAGE, EDITION, VOLUMES, SERIES, NUMBER, NOTE, PUBLISHER, LOCATION, ISBN, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    REPORT(
        "report",
        "A technical report, research report, or white paper published by a university or other institution.",
        arrayOf(AUTHOR, TITLE, TYPE, INSTITUTION, DATE),
        arrayOf(YEAR, SUBTITLE, TITLEADDON, LANGUAGE, NUMBER, VERSION, NOTE, LOCATION, MONTH, ISRN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    SOFTWARE(
        "software",
        "Computer software. The standard styles will treat this entry type as an alias for\n" +
            "@misc.",
        arrayOf(TITLE, DATE),
        arrayOf(AUTHOR, EDITOR, YEAR, SUBTITLE, TITLEADDON, LANGUAGE, HOWPUBLISHED, TYPE, VERSION, NOTE, ORGANISATION, LOCATION, MONTH, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_THESIS(
        "thesis",
        "A thesis written for an educational institution to satisfy the requirements for a degree.",
        arrayOf(AUTHOR, TITLE, TYPE, INSTITUTION, DATE),
        arrayOf(YEAR, SUBTITLE, TITLEADDON, LANGUAGE, NOTE, LOCATION, MONTH, ISBN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_UNPUBLISHED(
        "unpublished",
        "A work with an author and a title which has not been formally published.",
        arrayOf(AUTHOR, TITLE, DATE),
        arrayOf(SUBTITLE, TITLEADDON, TYPE, EVENTTITLE, EVENTTITLEADDON, EVENTDATE, VENUE, LANGUAGE, HOWPUBLISHED, NOTE, LOCATION, ISBN, MONTH, ADDENDUM, PUBSTATE, URL, URLDATE),
        BIBLATEX
    ),
    BIBLATEX_CONFERENCE(
        "conference",
        "A legacy alias for @inproceedings.",
        arrayOf(AUTHOR, TITLE, BOOKTITLE, DATE),
        arrayOf(EDITOR, SUBTITLE, TITLEADDON, MAINTITLE, MAINSUBTITLE, MAINTITLEADDON, BOOKSUBTITLE, BOOKTITLEADDON, EVENTTITLE, EVENTTITLEADDON, EVENTDATE, VENUE, LANGUAGE, VOLUME, PART, VOLUMES, SERIES, NUMBER, NOTE, ORGANISATION, PUBLISHER, LOCATION, MONTH, ISBN, CHAPTER, PAGES, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    ELECTRONIC(
        "electronic",
        "An alias for @online.",
        arrayOf(AUTHOR, TITLE, DATE, URL),
        arrayOf(SUBTITLE, TITLEADDON, LANGUAGE, VERSION, NOTE, ORGANISATION, ADDENDUM, PUBSTATE, EPRINTCLASS, EPRINTTYPE, URLDATE),
        BIBLATEX
    ),
    MASTERSTHESIS(
        "mastersthesis",
        "Similar to @thesis except that the type field is optional and defaults to the\n" +
            "localised term ‘Master’s thesis’. You may still use the type field to override that.",
        arrayOf(AUTHOR, TITLE, INSTITUTION, DATE),
        arrayOf(YEAR, SUBTITLE, TITLEADDON, LANGUAGE, NOTE, LOCATION, MONTH, ISBN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE, TYPE),
        BIBLATEX
    ),
    BIBLATEX_PHDTHESIS(
        "phdthesis",
        "Similar to @thesis except that the type field is optional and defaults to the\n" +
            "localised term ‘PhD thesis’. You may still use the type field to override that.",
        arrayOf(AUTHOR, TITLE, INSTITUTION, DATE),
        arrayOf(YEAR, SUBTITLE, TITLEADDON, LANGUAGE, NOTE, LOCATION, MONTH, ISBN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE, TYPE),
        BIBLATEX
    ),
    BIBLATEX_TECHREPORT(
        "techreport",
        "Similar to @report except that the type field is optional and defaults to the\n" +
            "localised term ‘technical report’. You may still use the type field to override that.",
        arrayOf(AUTHOR, TITLE, TYPE, INSTITUTION, DATE),
        arrayOf(YEAR, SUBTITLE, TITLEADDON, LANGUAGE, NUMBER, VERSION, NOTE, LOCATION, MONTH, ISRN, CHAPTER, PAGES, PAGETOTAL, ADDENDUM, PUBSTATE, DOI, EPRINT, EPRINTCLASS, EPRINTTYPE, URL, URLDATE),
        BIBLATEX
    ),
    WWW(
        "www",
        "An alias for @online, provided for jurabib compatibility.",
        arrayOf(AUTHOR, TITLE, DATE, URL),
        arrayOf(SUBTITLE, TITLEADDON, LANGUAGE, VERSION, NOTE, ORGANISATION, ADDENDUM, PUBSTATE, EPRINTCLASS, EPRINTTYPE, URLDATE),
        BIBLATEX
    ),
    ;

    companion object {

        private val lookup: MutableMap<String, BibtexEntryType> = HashMap()

        init {
            for (entry in values()) {
                lookup[entry.token] = entry
            }
        }

        @JvmStatic
        operator fun get(token: String): BibtexEntryType? {
            var trimmedToken = token.lowercase(Locale.getDefault())
            if (token.startsWith("@")) {
                trimmedToken = token.substring(1)
            }
            return lookup[trimmedToken]
        }
    }
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Hannah Schellekens
 */
interface BibtexEntryField : Described, Dependend {

    /**
     * The name of the entry field as used as key in BibTeX tags.
     */
    val fieldName: String
}

package nl.hannahsten.texifyidea.lang

import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TextExpression

/**
 * @author Hannah Schellekens
 */
interface BibtexEntryType : Described, Dependend {

    /**
     * The token used to identify the BibTeX token _without_ `@` prefix.
     */
    val token: String

    /**
     * All required fields/keys. Empty array when no fields are required.
     */
    val required: Array<BibtexEntryField>

    /**
     * All optional fields/keys. Empty array when no fields are optional.
     */
    val optional: Array<BibtexEntryField>

    /**
     * Get all [required] and all [optional] fields. Empty collection when there are no required and no optional fields.
     */
    fun allFields(): Collection<BibtexEntryField> {
        val list: MutableList<BibtexEntryField> = ArrayList()
        list.addAll(required)
        list.addAll(optional)
        return list
    }

    fun bibPackage(): String = when (dependency) {
        LatexPackage.BIBLATEX -> "BIBLATEX"
        else -> "BIBTEX"
    }

    /**
     * Create a template to insert when inserting this BibTeX entry.
     */
    fun template(): TemplateImpl {
        val keyValueString = required.mapIndexed { i: Int, field: BibtexEntryField ->
            "${field.fieldName} = {\$__Variable${i + 1}\$}"
        }.joinToString(",\n")

        val templateString = "{\$__Variable0\$,\n$keyValueString,\$END\$\n}"

        val template = object : TemplateImpl("", templateString, "") {
            override fun isToReformat(): Boolean = true
        }
        template.addVariable(TextExpression("identifier"), true)
        required.forEach { template.addVariable(TextExpression(it.fieldName), true) }
        return template
    }
}

package nl.hannahsten.texifyidea.lang

import nl.hannahsten.texifyidea.lang.Environment.Context
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.ALGORITHMICX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.GAUSS
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.MATHTOOLS
import nl.hannahsten.texifyidea.lang.commands.Argument
import nl.hannahsten.texifyidea.lang.commands.OptionalArgument
import nl.hannahsten.texifyidea.lang.commands.RequiredArgument
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.parser.name
import java.util.*

/**
 * @author Hannah Schellekens, Sten Wessel
 */
enum class DefaultEnvironment(
    override val environmentName: String,
    override val initialContents: String = "",
    override val context: Context = Context.NORMAL,
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override vararg val arguments: Argument,
    override val description: String = ""
) : Environment {

    // Vanilla LaTeX
    ABSTRACT(environmentName = "abstract"),
    ALLTT(environmentName = "alltt"),
    ARRAY(environmentName = "array", arguments = arrayOf(RequiredArgument("cols")), context = Context.MATH),
    CENTER(environmentName = "center"),
    DESCRIPTION(environmentName = "description", initialContents = "\\item "),
    DISPLAYMATH(environmentName = "displaymath", context = Context.MATH),
    DOCUMENT(environmentName = "document"),
    ENUMERATE(environmentName = "enumerate", initialContents = "\\item "),
    EQUATION(environmentName = "equation", context = Context.MATH),
    EQNARRAY(environmentName = "eqnarray", context = Context.MATH),
    EQNARRAY_STAR(environmentName = "eqnarray*", context = Context.MATH),
    FIGURE(environmentName = "figure", arguments = arrayOf(OptionalArgument("placement"))),
    FIGURE_STAR(environmentName = "figure*", arguments = arrayOf(OptionalArgument("placement"))),
    FILECONTENTS(environmentName = "filecontents"),
    FILECONTENTS_STAR(environmentName = "filecontents*"),
    FLUSHLEFT(environmentName = "flushleft"),
    FLUSHRIGHT(environmentName = "flushright"),
    FOOTNOTESIZE(environmentName = "footnotesize"),
    HUGE(environmentName = "huge"),
    CAPITAL_HUGE(environmentName = "Huge"),
    ITEMIZE(environmentName = "itemize", initialContents = "\\item "),
    LARGE(environmentName = "large"),
    CAPITAL_LARGE(environmentName = "Large"),
    SCREAMING_LARGE(environmentName = "LARGE"),
    LIST(environmentName = "list", arguments = arrayOf(RequiredArgument("label"), RequiredArgument("spacing"))),
    LONGTABLE(environmentName = "longtable", arguments = arrayOf(RequiredArgument("cols"))),
    LRBOX(environmentName = "lrbox"),
    MATH(environmentName = "math"),
    MINIPAGE(environmentName = "minipage", arguments = arrayOf(OptionalArgument("position"), RequiredArgument("width"))),
    NORMALSIZE(environmentName = "normalsize"),
    QUOTATION(environmentName = "quotation"),
    QUOTE(environmentName = "quote"),
    SCRIPTSIZE(environmentName = "scriptsize"),
    SMALL(environmentName = "small"),
    TABBING(environmentName = "tabbing"),
    TABLE(environmentName = "table", arguments = arrayOf(OptionalArgument("placement"))),
    TABLE_STAR(environmentName = "table*", arguments = arrayOf(OptionalArgument("placement"))),
    TABU(environmentName = "tabu", arguments = arrayOf(RequiredArgument("cols"))),
    TABULAR(environmentName = "tabular", arguments = arrayOf(OptionalArgument("pos"), RequiredArgument("cols"))),
    TABULARX(environmentName = "tabularx", arguments = arrayOf(RequiredArgument("width"), RequiredArgument("cols"))),
    TABULARY(environmentName = "tabulary", arguments = arrayOf(RequiredArgument("length"), RequiredArgument("pream"))),
    TABULAR_STAR(environmentName = "tabular*", arguments = arrayOf(RequiredArgument("width"), OptionalArgument("pos"), RequiredArgument("cols"))),
    THEBIBLIOGRAPHY(environmentName = "thebibliography", arguments = arrayOf(RequiredArgument("widestlabel"))),
    THEINDEX(environmentName = "theindex"),
    THEOREM(environmentName = "theorem", arguments = arrayOf(OptionalArgument("optional"))),
    TINY(environmentName = "tiny"),
    TITLEPAGE(environmentName = "titlepage"),
    TRIVLIST(environmentName = "trivlist"),
    VERBATIM(environmentName = "verbatim"),
    VERBATIM_CAPITAL(environmentName = "Verbatim"),
    VERBATIM_STAR(environmentName = "verbatim*"),
    VERSE(environmentName = "verse"),

    // amsmath
    ALIGN(environmentName = "align", context = Context.MATH, dependency = AMSMATH),
    ALIGNAT(environmentName = "alignat", context = Context.MATH, dependency = AMSMATH),
    ALIGNAT_STAR(environmentName = "alignat*", context = Context.MATH, dependency = AMSMATH),
    ALIGNED(environmentName = "aligned", context = Context.MATH, dependency = AMSMATH),
    ALIGNEDAT(environmentName = "alignedat", context = Context.MATH, dependency = AMSMATH),
    ALIGN_STAR(environmentName = "align*", context = Context.MATH, dependency = AMSMATH),
    BMATRIX(environmentName = "bmatrix", context = Context.MATH, dependency = AMSMATH),
    BMATRIX_CAPITAL(environmentName = "Bmatrix", context = Context.MATH, dependency = AMSMATH),
    CASES(environmentName = "cases", context = Context.MATH, dependency = AMSMATH),
    EQUATION_STAR(environmentName = "equation*", context = Context.MATH, dependency = AMSMATH),
    FLALIGN(environmentName = "flalign", context = Context.MATH, dependency = AMSMATH),
    FLALIGN_STAR(environmentName = "flalign*", context = Context.MATH, dependency = AMSMATH),
    GATHER(environmentName = "gather", context = Context.MATH, dependency = AMSMATH),
    GATHERED(environmentName = "gathered", context = Context.MATH, dependency = AMSMATH),
    GATHER_STAR(environmentName = "gather*", context = Context.MATH, dependency = AMSMATH),
    MATRIX(environmentName = "matrix", context = Context.MATH, dependency = AMSMATH),
    MULTLINE(environmentName = "multline", context = Context.MATH, dependency = AMSMATH),
    MULTLINE_STAR(environmentName = "multline*", context = Context.MATH, dependency = AMSMATH),
    PMATRIX(environmentName = "pmatrix", context = Context.MATH, dependency = AMSMATH),
    SMALLMATRIX(environmentName = "smallmatrix", context = Context.MATH, dependency = AMSMATH),
    SPLIT(environmentName = "split", context = Context.MATH, dependency = AMSMATH),
    SUBARRAY(environmentName = "subarray", context = Context.MATH, dependency = AMSMATH),
    SUBEQUATIONS(environmentName = "subequations", dependency = AMSMATH),
    VMATRIX(environmentName = "vmatrix", context = Context.MATH, dependency = AMSMATH),
    VMATRIX_CAPITAL(environmentName = "Vmatrix", context = Context.MATH, dependency = AMSMATH),
    XALIGNAT(environmentName = "xalignat", context = Context.MATH, dependency = AMSMATH),
    XALIGNAT_STAR(environmentName = "xalignat*", context = Context.MATH, dependency = AMSMATH),
    XXALIGNAT(environmentName = "xxalignat", context = Context.MATH, dependency = AMSMATH),

    // mathtools
    MATRIX_STAR(environmentName = "matrix*", context = Context.MATH, dependency = MATHTOOLS),
    PMATRIX_STAR(environmentName = "pmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    BMATRIX_STAR(environmentName = "bmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    BMATRIX_CAPITAL_STAR(environmentName = "Bmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    VMATRIX_STAR(environmentName = "vmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    VMATRIX_CAPITAL_STAR(environmentName = "Vmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    SMALLMATRIX_STAR(environmentName = "smallmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    PSMALLMATRIX(environmentName = "psmallmatrix", context = Context.MATH, dependency = MATHTOOLS),
    PSMALLMATRIX_STAR(environmentName = "psmallmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    BSMALLMATRIX(environmentName = "bsmallmatrix", context = Context.MATH, dependency = MATHTOOLS),
    BSMALLMATRIX_STAR(environmentName = "bsmallmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    BSMALLMATRIX_CAPITAL(environmentName = "Bsmallmatrix", context = Context.MATH, dependency = MATHTOOLS),
    BSMALLMATRIX_CAPITAL_STAR(environmentName = "Bsmallmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    VSMALLMATRIX(environmentName = "vsmallmatrix", context = Context.MATH, dependency = MATHTOOLS),
    VSMALLMATRIX_STAR(environmentName = "vsmallmatrix*", context = Context.MATH, dependency = MATHTOOLS),
    VSMALLMATRIX_CAPITAL(environmentName = "Vsmallmatrix", context = Context.MATH, dependency = MATHTOOLS),
    VSMALLMATRIX_CAPITAL_STAR(environmentName = "Vsmallmatrix*", context = Context.MATH, dependency = MATHTOOLS),

    // other
    ALGORITHM("algorithm"),
    ALGORITHMIC("algorithmic", dependency = ALGORITHMICX),
    BLOCKARRAY(environmentName = "blockarray", dependency = LatexPackage.BLKARRAY),
    BLOCK(environmentName = "block", dependency = LatexPackage.BLKARRAY),
    GMATRIX(environmentName = "gmatrix", context = Context.MATH, dependency = GAUSS),
    COMMENT(environmentName = "comment", context = Context.COMMENT, dependency = LatexPackage.COMMENT),
    LISTINGS(environmentName = "lstlisting", dependency = LatexPackage.LISTINGS),
    LONGTBLR(environmentName = "longtblr", dependency = LatexPackage.TABULARRAY, arguments = arrayOf(OptionalArgument("outer"), RequiredArgument("inner"))),
    LUACODE(environmentName = "luacode", dependency = LatexPackage.LUACODE),
    LUACODE_STAR(environmentName = "luacode*", dependency = LatexPackage.LUACODE),
    PYCODE(environmentName = "pycode", dependency = LatexPackage.PYTHONTEX),
    TALLTBLR(environmentName = "talltblr", dependency = LatexPackage.TABULARRAY, arguments = arrayOf(OptionalArgument("outer"), RequiredArgument("inner"))),
    TBLR(environmentName = "tblr", dependency = LatexPackage.TABULARRAY, arguments = arrayOf(OptionalArgument("outer"), RequiredArgument("inner"))),
    TESTCOLORS(environmentName = "testcolors", initialContents = "", context = Context.NORMAL, dependency = LatexPackage.XCOLOR, arguments = arrayOf(OptionalArgument("num models"))),
    TIKZPICTURE(environmentName = "tikzpicture", dependency = LatexPackage.TIKZ),
    WIDETABULAR(environmentName = "widetabular", dependency = LatexPackage.WIDETABLE),
    WIDETABLE(environmentName = "widetable", dependency = LatexPackage.WIDETABLE),
    ;

    companion object {

        /**
         * Caches all the enum instances to increase performance.
         */
        private val lookup = HashMap<String, DefaultEnvironment>()

        init {
            for (environment in values()) {
                lookup[environment.environmentName] = environment
            }
        }

        /**
         * Gets the [DefaultEnvironment] object that corresponds to the given Psi environment.
         */
        @JvmStatic
        fun fromPsi(latexEnvironment: LatexEnvironment): DefaultEnvironment? {
            val text: String = latexEnvironment.name()?.text ?: return null
            return get(text.lowercase(Locale.getDefault()))
        }

        /**
         * Finds the [DefaultEnvironment] object with the given name.
         */
        @JvmStatic
        operator fun get(name: String): DefaultEnvironment? = lookup[name]
    }
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Hannah Schellekens
 */
interface Dependend {

    /**
     * Get the package that is required for the object to work.
     *
     * @return The package object, or [LatexPackage.DEFAULT] when no package is needed.
     */
    val dependency: LatexPackage
}

package nl.hannahsten.texifyidea.lang

/**
 * Marks objects that have a description.
 *
 * @author Hannah Schellekens
 */
interface Described {

    /**
     * A short description of what the object is for.
     */
    val description: String
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Sten Wessel
 */
interface Diacritic {

    companion object {

        fun buildChain(base: String, diacritics: List<Diacritic?>): String? {
            if (diacritics.contains(null) || diacritics.isEmpty()) {
                return null
            }
            return diacritics.fold(base) { s, d -> d!!.buildCommand(s) }
        }

        fun allValues(): List<Diacritic> {
            val list: MutableList<Diacritic> = ArrayList()
            list.addAll(Normal.values())
            list.addAll(Math.values())
            return list
        }
    }

    val unicode: String
    val command: String
    val needsSpace: Boolean
    val isTypeable: Boolean

    fun buildCommand(param: String) = command + if (param.length > 1) {
        "{$param}"
    }
    else if (needsSpace) {
        " $param"
    }
    else param

    enum class Normal(
        override val unicode: String,
        override val command: String,
        override val needsSpace: Boolean,
        override val isTypeable: Boolean = false
    ) : Diacritic {

        GRAVE("\u0300", "\\`", false, isTypeable = true),
        ACUTE("\u0301", "\\'", false, isTypeable = true),
        CIRCUMFLEX("\u0302", "\\^", false, isTypeable = true),
        DIERESIS("\u0308", "\\\"", false, isTypeable = true),
        DOUBLE_ACUTE("\u030B", "\\H", true),
        TILDE("\u0303", "\\~", false, isTypeable = true),
        CEDILLA("\u0327", "\\c", true, isTypeable = true),
        OGONEK("\u0328", "\\k", true),
        MACRON("\u0304", "\\=", false),
        MACRON_BELOW("\u0331", "\b", true),
        DOT("\u0307", "\\.", false),
        DOT_BELOW("\u0323", "\\d", true),
        RING("\u030A", "\\r", true),
        BREVE("\u0306", "\\u", true),
        CARON("\u030C", "\\v", true),
        TIE("\u0311", "\\t", true),
        DOUBLE_TIE("\u0361", "\\t", true);

        companion object {

            fun fromUnicode(unicode: String): Normal? {
                return values()
                    .find { it.unicode == unicode }
            }

            fun fromCommand(command: String): Normal? {
                return values()
                    .find { it.command == command }
            }
        }

        override fun buildCommand(param: String): String {
            if (!param.startsWith("\\")) {
                return super.buildCommand(param.replace("i", "\\i").replace("j", "\\j"))
            }

            return super.buildCommand(param)
        }
    }

    enum class Math(
        override val unicode: String, override val command: String, override val needsSpace: Boolean,
        override val isTypeable: Boolean = false
    ) : Diacritic {

        HAT("\u0302", "\\hat", true),
        DOUBLE_HAT("\u1DCD", "\\widehat", true),
        CHECK("\u030C", "\\check", true),
        TILDE("\u0303", "\\tilde", true),
        DOUBLE_TILDE("\u0360", "\\widetilde", true),
        ACUTE("\u0301", "\\acute", true),
        GRAVE("\u0300", "\\grave", true),
        DOT("\u0307", "\\dot", true),
        DOUBLE_DOT("\u0308", "\\ddot", true),
        BREVE("\u0306", "\\breve", true),
        BAR("\u0304", "\\bar", true),
        VEC("\u20D7", "\\vec", true);

        companion object {

            fun fromUnicode(unicode: String): Math? {
                return values()
                    .find { it.unicode == unicode }
            }

            fun fromCommand(command: String): Math? {
                return values()
                    .find { it.command == command }
            }
        }

        override fun buildCommand(param: String): String {
            if (!param.startsWith("\\")) {
                return super.buildCommand(param.replace("i", "\\imath").replace("j", "\\jmath"))
            }

            return super.buildCommand(param)
        }
    }
}

package nl.hannahsten.texifyidea.lang

import com.intellij.openapi.project.Project
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import nl.hannahsten.texifyidea.index.file.LatexExternalEnvironmentIndex
import nl.hannahsten.texifyidea.lang.commands.Argument
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.*
import nl.hannahsten.texifyidea.util.files.removeFileExtension
import nl.hannahsten.texifyidea.util.startsWithAny

/**
 * @author Hannah Schellekens
 */
interface Environment : Dependend, Described {

    companion object {

        /**
         * Looks up a default environment by the given name.
         *
         * @param environmentName
         *              The name of the environment object to get.
         * @return The [DefaultEnvironment] with the given name, or `null` when it couldn't
         * be found.
         */
        fun lookup(environmentName: String) = DefaultEnvironment[environmentName]

        /**
         * Create an [Environment] for the given environment name.
         * See [LatexCommand.lookupInIndex].
         */
        fun lookupInIndex(environmentName: String, project: Project): Set<Environment> {
            val envs = mutableSetOf<Environment>()
            FileBasedIndex.getInstance().processValues(
                LatexExternalEnvironmentIndex.id, environmentName, null, { file, value ->
                val dependency = file.name.removeFileExtension()
                val env = object : Environment {
                    override val arguments = extractArgumentsFromDocs(value)
                    override val description = value
                    override val dependency =
                        if (dependency.isBlank()) LatexPackage.DEFAULT else LatexPackage.create(file)
                    override val context = Context.NORMAL
                    override val initialContents = ""
                    override val environmentName = environmentName
                }
                envs.add(env)
                true
            },
                GlobalSearchScope.everythingScope(project)
            )

            // See LatexCommand#lookUpInIndex()
            return envs.distinctBy { listOf(it.environmentName, it.dependency, it.context, it.description).plus(it.arguments) }
                .groupBy { listOf(it.environmentName, it.dependency, it.context) }
                .mapValues { it.value.maxByOrNull { cmd -> cmd.description }!! }
                .values.toSet()
        }

        fun extractArgumentsFromDocs(docs: String): Array<Argument> {
            // Maybe the arguments are given right at the beginning of the docs
            val argCommands = arrayOf(OARG, MARG, PARG).map { it.commandWithSlash }.toTypedArray()
            if (docs.startsWithAny(*argCommands)) {
                return LatexCommand.getArgumentsFromStartOfString(docs, 0)
            }

            return emptyArray()
        }

        /**
         * @see [lookup]
         */
        operator fun get(environmentName: String) = lookup(environmentName)
    }

    /**
     * Get what type of context this enviroment has inside.
     */
    val context: Context

    /**
     * Get the contents that must be placed into the environment just after it has been
     * inserted using the auto complete.
     */
    val initialContents: String

    /**
     * Get the name of the environment.
     */
    val environmentName: String

    /**
     * Get all the environment myArguments.
     */
    val arguments: Array<out Argument>

    /**
     * Concatenates all arguments to each other.
     *
     * @return e.g. `{ARG1}{ARG2}[ARG3]`
     */
    @Suppress("KDocUnresolvedReference")
    fun getArgumentsDisplay(): String {
        val sb = StringBuilder()
        for (arg in arguments) {
            sb.append(arg.toString())
        }

        return sb.toString()
    }

    /**
     * @author Hannah Schellekens
     */
    enum class Context {

        NORMAL,
        MATH,
        COMMENT
    }
}

package nl.hannahsten.texifyidea.lang

/**
 * Information about a custom commands which has a \label command in the definition.
 */
data class LabelingCommandInformation(
    /** Parameter positions which define a label, starting from 0 (note: LaTeX starts from 1).
     * For example in \newcommand{\mylabel}[2]{\section{#1}\label{#2}} it is position 1. */
    var positions: List<Int>,
    /** True if the first label parameter labels a previous command, for example in \newcommand{\mylabel}[1]{\label{#1}}
     * but not in \newcommand{\mylabel}[2]{\section{#1}\label{#2}} */
    var labelsPreviousCommand: Boolean,
    /** Default label prefix, for example in \newcommand{\mylabel}[1]{\label{sec:#1}} it would be sec: */
    var prefix: String = ""
)

package nl.hannahsten.texifyidea.lang

/**
 * All LaTeX document classes for which TeXiFy has special behaviour.
 *
 * @author Thomas
 */
open class LatexDocumentClass(
    val name: String,
    vararg val parameters: String = emptyArray()
) {

    companion object {

        val EXAM = LatexDocumentClass("exam")
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) {
            return true
        }
        if (other !is LatexDocumentClass) {
            return false
        }

        val aPackage = other as LatexDocumentClass?
        return name == aPackage!!.name
    }

    override fun hashCode() = name.hashCode()

    override fun toString() = "DocumentClass{$name}"
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Sten Wessel
 */
enum class LatexMode {

    NORMAL,
    MATH,
    ENVIRONMENT_NAME
}

package nl.hannahsten.texifyidea.lang

import com.intellij.codeInsight.intention.FileModifier
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.file.StyleFileType
import nl.hannahsten.texifyidea.util.files.removeFileExtension

/**
 * @author Hannah Schellekens
 */
@FileModifier.SafeTypeForPreview
open class LatexPackage @JvmOverloads constructor(
    val name: String,
    vararg val parameters: String = emptyArray(),
    /**
     * Source (dtx/sty) filename without extension.
     * Since a package can consist of multiple source/doc files, we do want
     * to track in which source file a command is defined, for example to find
     * the matching pdf file.
     */
    val fileName: String = name,
) {

    companion object {

        // Predefined packages.
        val DEFAULT = LatexPackage("")

        val ALGORITHM2E = LatexPackage("algorithm2e")
        val ALGORITHMICX = LatexPackage("algorithmicx")
        val ALGPSEUDOCODE = LatexPackage("algpseudocode")
        val AMSFONTS = LatexPackage("amsfonts")
        val AMSMATH = LatexPackage("amsmath")
        val AMSSYMB = LatexPackage("amssymb")
        val BIBLATEX = LatexPackage("biblatex")
        val BLINDTEXT = LatexPackage("blindtext")
        val BLKARRAY = LatexPackage("blkarray")
        val BM = LatexPackage("bm")
        val BOOKTABS = LatexPackage("booktabs")
        val CHAPTERBIB = LatexPackage("chapterbib")
        val CLEVEREF = LatexPackage("cleveref")
        val COLOR = LatexPackage("color")
        val COMMENT = LatexPackage("comment")
        val CSQUOTES = LatexPackage("csquotes")
        val EUROSYM = LatexPackage("eurosym")
        val FLOAT = LatexPackage("float")
        val FONTENC = LatexPackage("fontenc")
        val GAUSS = LatexPackage("gauss")
        val GLOSSARIES = LatexPackage("glossaries")
        val GLOSSARIESEXTRA = LatexPackage("glossaries-extra")
        val GRAPHICS = LatexPackage("graphics")
        val GRAPHICX = LatexPackage("graphicx")
        val HVINDEX = LatexPackage("hvindex")
        val HYPERREF = LatexPackage("hyperref")
        val IDXLAYOUT = LatexPackage("idxlayout")
        val IMAKEIDX = LatexPackage("imakeidx")
        val IMPORT = LatexPackage("import")
        val INDEX = LatexPackage("index")
        val INDEXTOOLS = LatexPackage("indextools")
        val INPUTENC = LatexPackage("inputenc")
        val LATEXSYMB = LatexPackage("latexsymb")
        val LIPSUM = LatexPackage("lipsum")
        val LISTINGS = LatexPackage("listings")
        val LUACODE = LatexPackage("luacode")
        val MAKEIDX = LatexPackage("makeidx")
        val MARVOSYM = LatexPackage("marvosym")
        val MATHABX = LatexPackage("mathabx")
        val MATHTOOLS = LatexPackage("mathtools")
        val MULTIND = LatexPackage("multind")
        val NATBIB = LatexPackage("natbib")
        val NEWTXMATH = LatexPackage("newtxmath")
        val NTHEOREM = LatexPackage("ntheorem")
        val PDFCOMMENT = LatexPackage("pdfcomment")
        val PYTHONTEX = LatexPackage("pythontex")
        val REPEATINDEX = LatexPackage("repeatindex")
        val SIUNITX = LatexPackage("siunitx")
        val SPLITIDX = LatexPackage("splitidx")
        val SPLITINDEX = LatexPackage("splitindex")
        val STMARYRD = LatexPackage("stmaryrd")
        val SUBFILES = LatexPackage("subfiles")
        val TABULARRAY = LatexPackage("tabularray")
        val TCOLORBOX = LatexPackage("tcolorbox")
        val TEXTCOMP = LatexPackage("textcomp")
        val TIKZ = LatexPackage("tikz")
        val ULEM = LatexPackage("ulem")
        val VARIOREF = LatexPackage("varioref")
        val WASYSYM = LatexPackage("wasysym")
        val WIDETABLE = LatexPackage("widetable")
        val XCOLOR = LatexPackage("xcolor")
        val XPARSE = LatexPackage("xparse")

        /**
         * Create package based on the source (dtx/sty) file.
         */
        fun create(sourceFile: VirtualFile): LatexPackage {
            val isLatexBase = sourceFile.parent.name == "base"
            val dependencyText = if (sourceFile.fileType is StyleFileType) {
                sourceFile.nameWithoutExtension
            }
            else {
                // The mapping from dtx to package names is nontrivial, we just do a guess for now
                sourceFile.parent.name
            }
            val fileName = sourceFile.name.removeFileExtension()
            return if (isLatexBase) LatexPackage("", fileName = fileName) else LatexPackage(dependencyText, fileName = fileName)
        }
    }

    /**
     * Checks if this package is the default package ('no package').
     *
     * @return `true` when is the default package, `false` if it is any other package.
     */
    val isDefault: Boolean
        get() = equals(DEFAULT)

    /**
     * The name of the package, or the empty string when this is the default package.
     */
    val displayString = if (isDefault) "" else name

    /**
     * Creates a new package object with the same name and with the given parameters.
     */
    fun with(vararg parameters: String) = LatexPackage(name, *parameters)

    override fun equals(other: Any?): Boolean {
        if (this === other) {
            return true
        }
        if (other !is LatexPackage) {
            return false
        }

        val aPackage = other as LatexPackage?
        return name == aPackage!!.name
    }

    override fun hashCode() = name.hashCode()

    override fun toString() = "Package{$name}"
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Hannah Schellekens
 */
data class SimpleBibtexEntryField(override val fieldName: String, override val description: String, override val dependency: LatexPackage = LatexPackage.DEFAULT) : BibtexEntryField {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is SimpleBibtexEntryField) return false

        return fieldName == other.fieldName
    }

    override fun hashCode() = fieldName.hashCode()
}

package nl.hannahsten.texifyidea.lang

import nl.hannahsten.texifyidea.lang.commands.Argument

/**
 * @author Hannah Schellekens
 */
class SimpleEnvironment(
    override val environmentName: String,
    override val context: Environment.Context = Environment.Context.NORMAL,
    override val initialContents: String = "",
    override val arguments: Array<out Argument> = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val description: String = ""
) : Environment {

    constructor(environmentName: String) : this(environmentName, context = Environment.Context.NORMAL)
}

package nl.hannahsten.texifyidea.modules

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.templates.LatexTemplatesFactory
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

/**
 * This class provides functionality to create default files from templates, which is useful for example when creating a new project.
 */
class DefaultFileCreator(
    /** The project to add the file to. */
    val project: Project,
    /** The directory path of the file (no seperator and no file name). */
    val path: String
) {

    /**
     * Creates the main.tex file and applies the default file template.
     */
    fun addMainFile(isBibtexEnabled: Boolean) {
        val fileName = "main.tex"
        val mainFilePath = path + File.separator + fileName
        val mainFile = File(mainFilePath)

        // Create main file.
        try {
            mainFile.createNewFile()
        }
        catch (e: IOException) {
            e.printStackTrace()
            throw TeXception("Problem with creating main .tex file.", e)
        }

        // Apply template.
        val template = if (isBibtexEnabled) {
            LatexTemplatesFactory.fileTemplateTexWithBib
        }
        else {
            LatexTemplatesFactory.fileTemplateTex
        }
        val templateText = LatexTemplatesFactory.getTemplateText(project, template, fileName)

        try {
            FileOutputStream(mainFile).use { outputStream -> outputStream.write(templateText.toByteArray()) }
        }
        catch (e: IOException) {
            e.printStackTrace()
            throw TeXception("Could not apply .tex template to main file.", e)
        }
    }

    /**
     * Creates the main.bib file and applies the default file template.
     */
    fun addBibFile() {
        val fileName = "main.bib"
        val bibFilePath = path + File.separator + fileName
        val bibFile = File(bibFilePath)

        try {
            bibFile.createNewFile()
        }
        catch (e: IOException) {
            e.printStackTrace()
            throw TeXception("Problem with creating main .bib file", e)
        }

        // Apply template.
        val template = LatexTemplatesFactory.fileTemplateBib
        val templateText = LatexTemplatesFactory.getTemplateText(project, template, fileName)

        try {
            FileOutputStream(bibFile).use { outputStream -> outputStream.write(templateText.toByteArray()) }
        }
        catch (e: IOException) {
            e.printStackTrace()
            throw TeXception("Could not apply .bib template to main bibliography file.", e)
        }
    }
}

package nl.hannahsten.texifyidea.modules

import com.intellij.ide.util.projectWizard.*
import com.intellij.openapi.Disposable
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.projectRoots.SdkTypeId
import com.intellij.openapi.roots.CompilerModuleExtension
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.util.Condition
import com.intellij.openapi.util.Pair
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import nl.hannahsten.texifyidea.settings.sdk.LatexSdk
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import java.io.File
import java.util.*

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexModuleBuilder : ModuleBuilder() {

    private val sourcePaths: List<Pair<String, String>>
        get() {
            val paths = ArrayList<Pair<String, String>>()
            val path = contentEntryPath + File.separator + "src"
            File(path).mkdirs()
            paths.add(Pair.create(path, ""))

            return paths
        }

    var isBibtexEnabled = false

    override fun getModuleType() = LatexModuleType.INSTANCE

    override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?) = LatexModuleWizardStep(this)

    override fun modifyProjectTypeStep(settingsStep: SettingsStep): ModuleWizardStep {
        val filter = Condition { id: SdkTypeId -> id is LatexSdk }
        // Note that SdkSettingsStep is part of the java plugin so not available in non-IntelliJ IDEs
        // But we trust that this class won't be loaded in non-IJ
        return SdkSettingsStep(settingsStep, this, filter)
    }

    @Throws(ConfigurationException::class)
    override fun setupRootModel(rootModel: ModifiableRootModel) {
        val project = rootModel.project
        val fileSystem = LocalFileSystem.getInstance()
        val compilerModuleExtension = rootModel.getModuleExtension(CompilerModuleExtension::class.java)
        compilerModuleExtension.isExcludeOutput = true

        val contentEntry = doAddContentEntry(rootModel) ?: return

        for (sourcePath in sourcePaths) {
            val path = sourcePath.first
            File(path).mkdirs()
            val sourceRoot = fileSystem.refreshAndFindFileByPath(FileUtil.toSystemIndependentName(path))

            if (sourceRoot != null) {
                contentEntry.addSourceFolder(sourceRoot, false, sourcePath.second)
                DefaultFileCreator(project, path).addMainFile(isBibtexEnabled)
                if (isBibtexEnabled) {
                    DefaultFileCreator(project, path).addBibFile()
                }
            }
        }

        // Create source directory.
        for (sourcePath in sourcePaths) {
            val path = sourcePath.first
            File(path).mkdirs()

            val fileName = FileUtil.toSystemIndependentName(path)
            val sourceRoot = fileSystem.refreshAndFindFileByPath(fileName) ?: continue

            contentEntry.addSourceFolder(sourceRoot, false, sourcePath.second)
            DefaultFileCreator(project, path).addMainFile(isBibtexEnabled)
            if (isBibtexEnabled) {
                DefaultFileCreator(project, path).addBibFile()
            }
            fileSystem.refresh(true)
        }

        // Create output directory.
        var path = contentEntryPath + File.separator + "out"
        File(path).mkdirs()
        val outRoot = fileSystem.refreshAndFindFileByPath(FileUtil.toSystemIndependentName(path))
        if (outRoot != null) {
            contentEntry.addExcludeFolder(outRoot)
        }

        if (LatexSdkUtil.isMiktexAvailable) {
            // Create auxiliary directory.
            path = contentEntryPath + File.separator + "auxil"
            File(path).mkdirs()
            val auxRoot = fileSystem.refreshAndFindFileByPath(FileUtil.toSystemIndependentName(path))
            if (auxRoot != null) {
                contentEntry.addExcludeFolder(auxRoot)
            }
        }
    }
}

package nl.hannahsten.texifyidea.modules

import com.intellij.openapi.module.ModuleType
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * Note: ModuleTypes are deprecated, see [ModuleType].
 * Therefore, we do not register it in plugin.xml, so it won't show to the user.
 * We still use it for new project creation though.
 *
 * @author Sten Wessel
 */
class LatexModuleType : ModuleType<LatexModuleBuilder>(ID) {

    companion object {

        const val ID = "LATEX_MODULE_TYPE"

        val INSTANCE = LatexModuleType()
    }

    override fun createModuleBuilder() = LatexModuleBuilder()

    override fun getName() = "LaTeX"

    override fun getDescription() = "LaTeX"

    override fun getNodeIcon(isOpened: Boolean) = TexifyIcons.LATEX_MODULE
}

package nl.hannahsten.texifyidea.modules

import com.intellij.ide.util.projectWizard.ModuleWizardStep
import java.awt.FlowLayout
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JPanel

/**
 * @author Sten Wessel
 */
class LatexModuleWizardStep(private val builder: LatexModuleBuilder) : ModuleWizardStep() {

    private lateinit var bibtexEnabled: JCheckBox

    override fun getComponent(): JComponent {
        return JPanel(FlowLayout(FlowLayout.LEFT)).apply {
            bibtexEnabled = JCheckBox("Configure with BibTeX support")
            add(bibtexEnabled)
        }
    }

    override fun updateDataModel() {
        builder.isBibtexEnabled = bibtexEnabled.isSelected
    }
}

package nl.hannahsten.texifyidea.modules

import com.intellij.ide.util.projectWizard.AbstractNewProjectStep
import com.intellij.ide.util.projectWizard.CustomStepProjectGenerator
import com.intellij.ide.util.projectWizard.ProjectSettingsStepBase
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.roots.ModifiableModelsProvider
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.DirectoryProjectGenerator
import com.intellij.platform.DirectoryProjectGeneratorBase
import com.intellij.platform.ProjectGeneratorPeer
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import java.io.File

/**
 * This class registers the LaTeX module type especially for the creation of a new project in non-IntelliJ IDEs.
 * The creation of a new project in IntelliJ is handled by [LatexModuleType].
 *
 * At the moment of writing there seems to be no public official documentation about what needs to be done to register such a module type, but some helpful comments can be found at https://stackoverflow.com/a/49713551/4126843
 *
 * @author Thomas Schouten
 */
class LatexProjectGenerator :
    DirectoryProjectGeneratorBase<TexifySettings>(),
    CustomStepProjectGenerator<TexifySettings> {

    /** Keep a reference to the peer to get the value of the settings chosen by the user during project creation. */
    private var peer: LatexProjectGeneratorPeer? = null

    override fun createStep(
        projectGenerator: DirectoryProjectGenerator<TexifySettings>,
        callback: AbstractNewProjectStep.AbstractCallback<TexifySettings>
    ) = ProjectSettingsStepBase(projectGenerator, AbstractNewProjectStep.AbstractCallback())

    override fun getName() = "LaTeX"

    override fun getDescription() = "LaTeX"

    override fun getLogo() = TexifyIcons.LATEX_MODULE

    override fun createPeer(): ProjectGeneratorPeer<TexifySettings> = LatexProjectGeneratorPeer().also { peer = it }

    override fun generateProject(project: Project, baseDir: VirtualFile, settings: TexifySettings, module: Module) {
        // This behaviour was inspired by the Julia plugin, see
        // https://github.com/JuliaEditorSupport/julia-intellij/blob/master/src/org/ice1000/julia/lang/module/julia-projects.kt
        val modifiableModel: ModifiableRootModel = ModifiableModelsProvider.getInstance().getModuleModifiableModel(module)

        val rootModel = module.rootManager.modifiableModel

        /** Create a directory in a base directory unless it already exists. */
        fun findOrCreate(baseDir: VirtualFile, dir: String, module: Module) =
            baseDir.findChild(dir) ?: baseDir.createChildDirectory(module, dir)

        // We can only write to disk in a runWriteAction
        ApplicationManager.getApplication().runWriteAction {
            // Add source, auxil and output directories
            rootModel.contentEntries.firstOrNull()?.apply {
                addSourceFolder(findOrCreate(baseDir, "src", module), false)
                // TeX Live cannot handle an auxil/ folder
                if (LatexSdkUtil.isMiktexAvailable) {
                    addExcludeFolder(findOrCreate(baseDir, "auxil", module))
                }
                addExcludeFolder(findOrCreate(baseDir, "out", module))
            }

            val isBibtexEnabled: Boolean = peer?.bibtexEnabled?.isEnabled ?: false

            // Add a default LaTeX file
            val sourcePath = baseDir.path + File.separator + "src"
            DefaultFileCreator(project, sourcePath).addMainFile(isBibtexEnabled)

            if (isBibtexEnabled) {
                DefaultFileCreator(project, sourcePath).addBibFile()
            }

            rootModel.commit()

            ModifiableModelsProvider.getInstance().commitModuleModifiableModel(modifiableModel)
        }
    }
}

package nl.hannahsten.texifyidea.modules

import com.intellij.ide.util.projectWizard.SettingsStep
import com.intellij.platform.ProjectGeneratorPeer
import com.intellij.ui.components.JBCheckBox
import nl.hannahsten.texifyidea.settings.TexifySettings
import java.awt.FlowLayout
import javax.swing.JComponent
import javax.swing.JPanel

/**
 * Customize the project creation dialog, adding settings.
 *
 * Inspired by the Rust plugin, source:
 * https://github.com/intellij-rust/intellij-rust/blob/master/src/main/kotlin/org/rust/ide/newProject/RsProjectGeneratorPeer.kt
 */
class LatexProjectGeneratorPeer : ProjectGeneratorPeer<TexifySettings> {

    lateinit var bibtexEnabled: JBCheckBox

    private val settings = TexifySettings()
    private val listeners = ArrayList<ProjectGeneratorPeer.SettingsListener>()

    override fun validate(): Nothing? = null

    override fun getSettings() = settings

    override fun buildUI(settingsStep: SettingsStep) = settingsStep.addExpertPanel(component)

    override fun isBackgroundJobRunning() = false

    override fun addSettingsListener(listener: ProjectGeneratorPeer.SettingsListener) {
        listeners += listener
    }

    override fun getComponent(): JComponent {
        return JPanel(FlowLayout(FlowLayout.LEFT)).apply {
            bibtexEnabled = JBCheckBox("Configure with BibTeX support")
            add(bibtexEnabled)
        }
    }
}

package nl.hannahsten.texifyidea.modules

import com.intellij.ide.projectWizard.ProjectSettingsStep
import com.intellij.ide.util.importProject.ModuleDescriptor
import com.intellij.ide.util.importProject.ProjectDescriptor
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.ide.util.projectWizard.SdkSettingsStep
import com.intellij.ide.util.projectWizard.importSources.DetectedProjectRoot
import com.intellij.ide.util.projectWizard.importSources.ProjectFromSourcesBuilder
import com.intellij.ide.util.projectWizard.importSources.ProjectStructureDetector
import com.intellij.openapi.projectRoots.SdkTypeId
import com.intellij.openapi.util.Condition
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.settings.sdk.LatexSdk
import java.io.File
import javax.swing.Icon

/**
 * Support 'create project from existing sources.'
 * Docs: https://plugins.jetbrains.com/docs/intellij/project-wizard.html#implementing-project-structure-detector
 */
class LatexProjectStructureDetector : ProjectStructureDetector() {

    override fun detectRoots(
        dir: File,
        children: Array<out File>,
        base: File,
        result: MutableList<DetectedProjectRoot>
    ): DirectoryProcessingResult {
        // We only add a LaTeX module when there are no other modules,
        // so we create it on the project root to make the Project view work (it apparently needs at least one top-level module?)
        if (children.any { it.extension == LatexFileType.defaultExtension }) {
            result.add(object : DetectedProjectRoot(base) {
                override fun getRootTypeName() = "LaTeX"
            })
            return DirectoryProcessingResult.SKIP_CHILDREN
        }
        return DirectoryProcessingResult.PROCESS_CHILDREN
    }

    override fun createWizardSteps(
        builder: ProjectFromSourcesBuilder?,
        projectDescriptor: ProjectDescriptor?,
        stepIcon: Icon?
    ): MutableList<ModuleWizardStep> {
        // Note that at least one DetectedProjectRoot needs to be added as above for the steps to be actually added
        // (to avoid showing the step for non-LaTeX project creations)

        // Custom step with TeXiFy-specific options
        // At the moment it contains no settings which are relevant for creation from existing sources, so we leave it out
//        val customStep = LatexModuleWizardStep(LatexModuleBuilder())

        // Step to set up SDK
        val filter = Condition { id: SdkTypeId -> id is LatexSdk }
        val sdkSettingsStep = ProjectSettingsStep(builder?.context)
        val sdkSetupStep = SdkSettingsStep(sdkSettingsStep, LatexModuleBuilder(), filter)
        return mutableListOf(sdkSetupStep)
    }

    override fun setupProjectStructure(
        roots: MutableCollection<DetectedProjectRoot>,
        projectDescriptor: ProjectDescriptor,
        builder: ProjectFromSourcesBuilder
    ) {
        if (projectDescriptor.modules.isEmpty() && !builder.hasRootsFromOtherDetectors(this)) {
            val modules = roots.map {
                ModuleDescriptor(it.directory, LatexModuleType.INSTANCE, emptyList())
            }
            projectDescriptor.modules = modules
        }
    }

    override fun getDetectorId() = "LaTeX"
}

package nl.hannahsten.texifyidea.navigation

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.findCommandDefinitions
import nl.hannahsten.texifyidea.util.parser.forcedFirstRequiredParameterAsCommand

/**
 * @author Hannah Schellekens
 */
class GotoCommandDefinitionSymbolContributor : TexifyGotoSymbolBase<LatexCommands>() {

    override fun Project.findElements() = findCommandDefinitions()

    override fun LatexCommands.extractName() = forcedFirstRequiredParameterAsCommand()?.name

    override fun LatexCommands.createNavigationItem() = NavigationItemUtil.createCommandDefinitionNavigationItem(this)
}

package nl.hannahsten.texifyidea.navigation

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.findEnvironmentDefinitions
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * @author Hannah Schellekens
 */
class GotoEnvironmentDefinitionSymbolContributor : TexifyGotoSymbolBase<LatexCommands>() {

    override fun Project.findElements() = findEnvironmentDefinitions()

    override fun LatexCommands.extractName() = requiredParameter(0)

    override fun LatexCommands.createNavigationItem() = NavigationItemUtil.createEnvironmentDefinitionNavigationItem(this)
}

package nl.hannahsten.texifyidea.navigation

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.util.labels.extractLabelName
import nl.hannahsten.texifyidea.util.labels.findAllLabelsAndBibtexIds

/**
 * @author Hannah Schellekens
 */
class GotoLabelSymbolContributor : TexifyGotoSymbolBase<PsiElement>() {

    override fun Project.findElements() = findAllLabelsAndBibtexIds()

    override fun PsiElement.extractName() = extractLabelName()

    override fun PsiElement.createNavigationItem() = NavigationItemUtil.createLabelNavigationItem(this)
}

package nl.hannahsten.texifyidea.navigation

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.findSectionMarkers
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * @author Hannah Schellekens
 */
class GotoSectionDefinitionSymbolContributor : TexifyGotoSymbolBase<LatexCommands>() {

    override fun Project.findElements() = findSectionMarkers()

    override fun LatexCommands.extractName() = requiredParameter(0)

    override fun LatexCommands.createNavigationItem() = NavigationItemUtil.createSectionMarkerNavigationItem(this)
}

package nl.hannahsten.texifyidea.navigation

import com.intellij.navigation.NavigationItem
import com.intellij.psi.PsiElement
import com.intellij.util.xml.model.gotosymbol.GoToSymbolProvider
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.labels.extractLabelName
import nl.hannahsten.texifyidea.util.parser.forcedFirstRequiredParameterAsCommand
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands

/**
 * @author Hannah Schellekens
 */
object NavigationItemUtil {

    @JvmStatic
    fun createLabelNavigationItem(psiElement: PsiElement): NavigationItem? {
        return when (psiElement) {
            is LatexCommands -> {
                val text = psiElement.extractLabelName()
                if (text == "") {
                    return null
                }
                return GoToSymbolProvider.BaseNavigationItem(
                    psiElement,
                    text,
                    if (psiElement.name in psiElement.project.getLabelDefinitionCommands()) {
                        TexifyIcons.DOT_LABEL
                    }
                    else TexifyIcons.DOT_BIB
                )
            }
            is BibtexEntry -> GoToSymbolProvider.BaseNavigationItem(
                psiElement,
                psiElement.name ?: return null,
                TexifyIcons.DOT_BIB
            )
            else -> null
        }
    }

    @JvmStatic
    fun createCommandDefinitionNavigationItem(psiElement: LatexCommands): NavigationItem? {
        val defined = psiElement.forcedFirstRequiredParameterAsCommand() ?: return null
        return GoToSymbolProvider.BaseNavigationItem(psiElement, defined.name ?: "", TexifyIcons.DOT_COMMAND)
    }

    @JvmStatic
    fun createEnvironmentDefinitionNavigationItem(psiElement: LatexCommands): NavigationItem? {
        val environmentName = psiElement.requiredParameter(0) ?: return null
        return GoToSymbolProvider.BaseNavigationItem(psiElement, environmentName, TexifyIcons.DOT_ENVIRONMENT)
    }

    @JvmStatic
    fun createSectionMarkerNavigationItem(psiElement: LatexCommands): NavigationItem? {
        val sectionName = psiElement.requiredParameter(0) ?: return null
        val icon = when (psiElement.commandToken.text) {
            "\\part" -> TexifyIcons.DOT_PART
            "\\chapter" -> TexifyIcons.DOT_CHAPTER
            "\\subsection" -> TexifyIcons.DOT_SUBSECTION
            "\\subsubsection" -> TexifyIcons.DOT_SUBSUBSECTION
            "\\paragraph" -> TexifyIcons.DOT_PARAGRAPH
            "\\subparagraph" -> TexifyIcons.DOT_SUBPARAGRAPH
            // Also catches \section.
            else -> TexifyIcons.DOT_SECTION
        }
        return GoToSymbolProvider.BaseNavigationItem(psiElement, sectionName, icon)
    }
}

package nl.hannahsten.texifyidea.navigation

import com.intellij.navigation.ChooseByNameContributor
import com.intellij.navigation.NavigationItem
import com.intellij.openapi.project.Project

/**
 * @author Hannah Schellekens
 */
abstract class TexifyGotoSymbolBase<T> : ChooseByNameContributor {

    /**
     * Finds all elements that can be found in 'Goto symbol'.
     */
    abstract fun Project.findElements(): Iterable<T>

    /**
     * Transforms the element into its visible and identifyable name.
     * `null` when no name is available.
     */
    abstract fun T.extractName(): String?

    /**
     * Creates a navigation item from the given element.
     * `null` when no navigation item could be created.
     */
    abstract fun T.createNavigationItem(): NavigationItem?

    override fun getItemsByName(name: String?, pattern: String?, project: Project?, includeNonProjectItems: Boolean): Array<NavigationItem> {
        val elements = project?.findElements() ?: return emptyArray()
        return elements.asSequence()
            .map { it to it.extractName() }
            .filter { (_, definedName) ->
                definedName == name || (pattern != null && (definedName ?: "").contains(pattern, ignoreCase = true))
            }
            .mapNotNull { (psi, _) -> psi.createNavigationItem() }
            .toList()
            .toTypedArray()
    }

    override fun getNames(project: Project?, includeNonProjectItems: Boolean): Array<String> {
        val elements = project?.findElements() ?: return emptyArray()
        return elements.mapNotNull { it.extractName() }.toTypedArray()
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.grammar.BibtexLanguage

/**
 * @author Hannah Schellekens
 */
open class BibtexElementType(debugName: String) : IElementType(debugName, BibtexLanguage)

package nl.hannahsten.texifyidea.psi

import nl.hannahsten.texifyidea.index.stub.BibtexEntryStubElementType

/**
 * See LatexStubElementTypes
 */
interface BibtexStubElementTypes {

    companion object {

        @JvmField
        val ENTRY = BibtexTypes.ENTRY as BibtexEntryStubElementType
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.grammar.BibtexLanguage

/**
 * @author Hannah Schellekens
 */
open class BibtexTokenType(debugName: String) : IElementType(debugName, BibtexLanguage) {

    override fun toString() = "BibtexTokenType." + super.toString()
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.PsiElement

/**
 * This class allows the LatexCommandsImplMixin class to 'inject' methods into LatexCommands(Impl).
 * In general, it is more straightforward to provide extension methods in LatexCommandsUtil.
 */
interface LatexCommandWithParams : PsiElement {

    /**
     * Get the name of the command, for example \newcommand, from the stub if available, otherwise default to getting the text from psi.
     */
//    fun getName(): String?

    val parameterList: List<LatexParameter>

    /**
     * Generates a list of all names of all required parameters in the command.
     */
    fun getRequiredParameters(): List<String>

    fun getOptionalParameterMap(): Map<LatexOptionalKeyValKey, LatexKeyValValue?>
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import org.jetbrains.annotations.NonNls

/**
 * @author Sten Wessel
 */
class LatexElementType(@NonNls debugName: String) : IElementType(debugName, LatexLanguage)

package nl.hannahsten.texifyidea.psi

import com.intellij.openapi.util.TextRange
import com.intellij.psi.AbstractElementManipulator
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.util.PsiTreeUtil
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.psi.impl.LatexEnvironmentImpl

/**
 * Enable editing environment content in a separate window (when using language injection).
 */
class LatexEnvironmentManipulator : AbstractElementManipulator<LatexEnvironmentImpl>() {

    override fun handleContentChange(
        element: LatexEnvironmentImpl,
        range: TextRange,
        newContent: String
    ): LatexEnvironmentImpl? {
        val oldText = element.text
        // For some reason the endoffset of the given range is incorrect: sometimes it excludes the last line, so we calculate it ourselves
        val endOffset = oldText.indexOf("\\end{${element.getEnvironmentName()}}") - 1 // -1 to exclude \n
        val newText = oldText.substring(0, range.startOffset) + newContent + oldText.substring(endOffset)
        val file = PsiFileFactory.getInstance(element.project)
            .createFileFromText("temp.tex", LatexLanguage, newText)
        val res =
            PsiTreeUtil.findChildOfType(file, LatexEnvironmentImpl::class.java) ?: return null
        element.replace(res)
        return element
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.util.PsiTreeUtil
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.settings.conventions.LabelConventionType
import nl.hannahsten.texifyidea.settings.conventions.TexifyConventionsSettingsManager
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.getOptionalParameterMapFromParameters
import nl.hannahsten.texifyidea.util.parser.toStringMap

/*
* LatexEnvironment
*/
/**
 * Find the label of the environment. The method finds labels inside the environment content as well as labels
 * specified via an optional parameter
 *
 * @return the label name if any, null otherwise
 */
fun LatexEnvironment.getLabel(): String? {
    val stub = this.stub
    if (stub != null) return stub.label
    return if (EnvironmentMagic.labelAsParameter.contains(this.getEnvironmentName())) {
        // See if we can find a label option
        val optionalParameters = getOptionalParameterMapFromParameters(this.beginCommand.parameterList).toStringMap()
        optionalParameters.getOrDefault("label", null)
    }
    else {
        // Not very clean. We don't really need the conventions here, but determine which environments *can* have a
        // label. However, if we didn't use the conventions, we would have to duplicate the information in
        // EnvironmentMagic
        val conventionSettings = TexifyConventionsSettingsManager.getInstance(this.project).getSettings()
        if (conventionSettings.getLabelConvention(
                this.getEnvironmentName(),
                LabelConventionType.ENVIRONMENT
            ) == null
        ) return null

        val content = this.environmentContent ?: return null

        // See if we can find a label command inside the environment
        val children = PsiTreeUtil.findChildrenOfType(content, LatexCommands::class.java)
        if (!children.isEmpty()) {
            // We cannot include user defined labeling commands, because to get them we need the index,
            // but this code is used to create the index (for environments)
            val labelCommands = CommandMagic.labelDefinitionsWithoutCustomCommands
            val labelCommand =
                children.firstOrNull { c: LatexCommands -> labelCommands.contains(c.name) } ?: return null
            val requiredParameters = labelCommand.getRequiredParameters()
            if (requiredParameters.isEmpty()) return null
            val info = CommandManager.labelAliasesInfo.getOrDefault(labelCommand.name, null) ?: return null
            if (!info.labelsPreviousCommand) return null
            val parameterPosition = info.positions.firstOrNull() ?: 0
            return if (parameterPosition > requiredParameters.size - 1 || parameterPosition < 0) null else requiredParameters[parameterPosition]
        }
        null
    }
}

fun LatexEnvironment.getEnvironmentName(): String {
    val stub = this.stub
    if (stub != null) return stub.environmentName
    val parameters = this.beginCommand.parameterList
    if (parameters.isEmpty()) return ""
    val environmentNameParam = parameters[0]
    val requiredParam = environmentNameParam.requiredParam ?: return ""
    val contentList = requiredParam.requiredParamContentList
    if (contentList.isEmpty()) return ""
    val paramText = contentList[0].parameterText ?: return ""
    return paramText.text
}

package nl.hannahsten.texifyidea.psi

import com.intellij.lang.Language
import com.intellij.lang.LanguageParserDefinitions
import com.intellij.openapi.util.TextRange
import com.intellij.psi.InjectedLanguagePlaces
import com.intellij.psi.LanguageInjector
import com.intellij.psi.PsiLanguageInjectionHost
import nl.hannahsten.texifyidea.lang.magic.DefaultMagicKeys
import nl.hannahsten.texifyidea.lang.magic.magicComment
import nl.hannahsten.texifyidea.util.camelCase
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.toStringMap
import nl.hannahsten.texifyidea.util.remove
import java.util.*

/**
 * Inject language based on magic comments.
 *
 * @author Sten Wessel
 */
class LatexLanguageInjector : LanguageInjector {

    override fun getLanguagesToInject(host: PsiLanguageInjectionHost, registrar: InjectedLanguagePlaces) {
        if (host is LatexEnvironment) {
            val magicComment = host.magicComment()
            val hasMagicCommentKey = magicComment.containsKey(DefaultMagicKeys.INJECT_LANGUAGE)

            val languageId = when {
                hasMagicCommentKey -> {
                    magicComment.value(DefaultMagicKeys.INJECT_LANGUAGE)
                }
                host.getEnvironmentName() == "lstlisting" -> {
                    host.beginCommand.getOptionalParameterMap().toStringMap().getOrDefault("language", null)
                }
                host.getEnvironmentName() in EnvironmentMagic.languageInjections.keys -> {
                    EnvironmentMagic.languageInjections[host.getEnvironmentName()]
                }
                host.getEnvironmentName().endsWith("code", ignoreCase = false) -> {
                    // Environment may have been defined with the \newminted shortcut (see minted documentation)
                    host.getEnvironmentName().remove("code")
                }
                else -> {
                    null
                }
            } ?: return

            val language = findLanguage(languageId) ?: return

            // A parser definition is required
            if (LanguageParserDefinitions.INSTANCE.forLanguage(language) == null) return

            val range = host.environmentContent?.textRange?.shiftRight(-host.textOffset) ?: TextRange.EMPTY_RANGE

            return registrar.addPlace(language, range, null, null)
        }

        if (host is LatexParameter) {
            val parent = host.parentOfType(LatexCommands::class) ?: return

            val languageId = CommandMagic.languageInjections[parent.commandToken.text.substring(1)]
            val language = findLanguage(languageId) ?: return
            if (LanguageParserDefinitions.INSTANCE.forLanguage(language) == null) return
            val range = host.textRange
                .shiftRight(-host.textOffset)
                .let { TextRange(it.startOffset + 1, it.endOffset - 1) }

            return registrar.addPlace(language, range, null, null)
        }
    }

    private fun findLanguage(id: String?): Language? {
        return if (id.isNullOrBlank()) null
        else {
            Language.findLanguageByID(id)
                ?: Language.findLanguageByID(id.lowercase(Locale.getDefault()))
                ?: Language.findLanguageByID(id.uppercase(Locale.getDefault()))
                ?: Language.findLanguageByID(id.camelCase())
        }
    }
}

package nl.hannahsten.texifyidea.psi

import nl.hannahsten.texifyidea.lang.magic.*

/**
 * Get a [MagicComment] from a [LatexMagicComment] psi element. This magic comment always contains a single key and a single value.
 */
fun LatexMagicComment.getMagicComment(): MagicComment<String, String> =
    listOf(magicCommentToken.text).textBasedMagicCommentParser().parse()

/**
 * Get the [MagicKey] from a [LatexMagicComment] psi element.
 */
fun LatexMagicComment.key(): MagicKey<String> {
    val key = getMagicComment().keySet().firstOrNull() ?: ""
    return DefaultMagicKeys.values().firstOrNull { it.key == key } ?: CustomMagicKey(key)
}

/**
 * Get the value from a [LatexMagicComment] psi element.
 */
fun LatexMagicComment.value(): String? = getMagicComment().value(key())

package nl.hannahsten.texifyidea.psi

import com.intellij.lang.PsiBuilder
import com.intellij.lang.parser.GeneratedParserUtilBase
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic

@Suppress("FunctionName")
class LatexParserUtil : GeneratedParserUtilBase() {

    companion object {

        /**
         * Remap tokens inside verbatim environments to raw text.
         * Requires the lexer to be in a proper state before and after the environment.
         */
        @JvmStatic fun injection_env_content(builder: PsiBuilder, level: Int, rawText: Parser): Boolean {
            // This might be optimized by handling the tokens incrementally
            val beginText = builder.originalText.subSequence(
                builder.latestDoneMarker?.startOffset ?: return true,
                builder.latestDoneMarker?.endOffset ?: return true
            )
            val nameStart = beginText.indexOf('{') + 1
            val nameEnd = beginText.indexOf('}')
            if (nameStart >= nameEnd) return false

            val env = beginText.subSequence(nameStart, nameEnd).toString()

            if (!EnvironmentMagic.isProbablyVerbatim(env)) return false

            val startIndex = builder.currentOffset
            // Exclude the last newline, so it will stay a whitespace,
            // otherwise the formatter (LatexSpacingRules) will insert a
            // newline too much between environment content and \end
            val endIndex = builder.originalText.indexOf("\\end{$env}", startIndex) - 1

            // If there is nothing to remap, for example because there are only newlines, return false
            if (endIndex < startIndex) return false

            // Only remap \end and whitespace tokens, other ones are already raw text by the lexer
            // This makes sure the the optional argument of a verbatim environment is not by mistake also remapped to raw text
            // \end is remapped because the lexer only knows afterwards whether it ended the environment or not, and whitespace is remapped because this allows keeping the last whitespace for the formatter
            builder.setTokenTypeRemapper { token, start, end, _ ->
                if (startIndex <= start && end <= endIndex &&
                    (token == LatexTypes.END_TOKEN || token == LatexTypes.BEGIN_TOKEN || token == LatexTypes.OPEN_BRACE || token == LatexTypes.OPEN_BRACE || token == com.intellij.psi.TokenType.WHITE_SPACE)
                ) {
                    LatexTypes.RAW_TEXT_TOKEN
                }
                else {
                    token
                }
            }

            rawText.parse(builder, level)

            builder.setTokenTypeRemapper(null)

            return true
        }
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.tree.LeafPsiElement
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.psi.LatexTypes.*
import nl.hannahsten.texifyidea.util.Log
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.findFirstChild
import nl.hannahsten.texifyidea.util.parser.firstChildOfType

/**
 * As the IntelliJ SDK docs say, to replace or insert text it is easiest to create a dummy file,
 * fill it with the desired text, extract the psi elements and put them in the actual psi tree.
 * Don't use document.insertString because that will lead to various things (like inspections) working with an obsolete psi tree.
 */
class LatexPsiHelper(private val project: Project) {

    private fun createEnvironmentContent(): LatexEnvironmentContent {
        val environment = createFromText(
            "\\begin{figure}\n" +
                "        Placeholder\n" +
                "    \\end{figure}"
        ).firstChildOfType(LatexEnvironment::class)!!
        environment.environmentContent!!.firstChild.delete()
        return environment.environmentContent!!
    }

    private fun createLatexOptionalParam(): LatexParameter {
        return createFromText("\\usepackage[]{package}")
            .findFirstChild { c -> c is LatexParameter && c.optionalParam != null }!!
    }

    /**
     * Create a label command \label{labelName}.
     */
    fun createLabelCommand(labelName: String): PsiElement {
        val labelText = "\\label{$labelName}"
        val fileFromText = createFromText(labelText)
        return fileFromText.firstChild
    }

    private fun createKeyValuePairs(parameter: String): LatexOptionalKeyValPair {
        val commandText = "\\begin{lstlisting}[$parameter]"
        val environment = createFromText(commandText).firstChildOfType(LatexEnvironment::class)!!
        val optionalParam = environment.beginCommand.firstChildOfType(LatexOptionalParam::class)!!
        return optionalParam.optionalKeyValPairList[0]
    }

    /**
     * Create a PsiFile containing the given text.
     */
    fun createFromText(text: String): PsiFile =
        PsiFileFactory.getInstance(project).createFileFromText("DUMMY.tex", LatexLanguage, text, false, true)

    /**
     * Adds the supplied element to the content of the environment.
     * @param environment The environment whose content should be manipulated
     * @param element The element to be inserted
     * @param after If specified, the new element will be inserted after this element
     * @return The new element in the PSI tree. Note that this element is *not* necessarily equal
     * to the supplied element. For example, the new element might have an updated endOffset
     */
    fun addToContent(environment: LatexEnvironment, element: PsiElement, after: PsiElement? = null): PsiElement {
        if (environment.environmentContent == null) {
            environment.addAfter(createEnvironmentContent(), environment.beginCommand)
        }
        val environmentContent = environment.environmentContent!!

        return if (after != null) {
            environmentContent.addAfter(element, after)
        }
        else {
            environmentContent.add(element)
        }
    }

    fun createRequiredParameter(content: String): LatexRequiredParam {
        val commandText = "\\label{$content}"
        return createFromText(commandText).firstChildOfType(LatexRequiredParam::class)!!
    }

    fun createOptionalParameter(content: String): LatexOptionalParam? {
        val commandText = "\\section[$content]{$content}"
        return createFromText(commandText).firstChildOfType(LatexOptionalParam::class)
    }

    /**
     * Returns the LatexOptionalParam node that is supposed to contain the label key for the command.
     * If no such node exists yet, a new one is created at the correct position.
     */
    private fun getOrCreateLabelOptionalParameters(command: LatexCommandWithParams): LatexOptionalParam {
        // This is only a heuristic. We would actually need detailed information on which optional parameter is
        // supposed to hold the label key.
        val existingParameters = command.getOptionalParameterMap()
        if (existingParameters.isEmpty()) {
            if (command is LatexCommands) {
                // For commands insert an optional parameter right after the command name (in case the command has a
                // star, insert the parameter after the start)
                command.addAfter(
                    createLatexOptionalParam(),
                    command.childrenOfType<LeafPsiElement>().firstOrNull { it.elementType == STAR }
                        ?: command.commandToken
                )
            }
            else {
                // Otherwise assume that the command belongs to an environment and insert the optional parameter after
                // the first parameter (which is the environment name)
                command.addAfter(createLatexOptionalParam(), command.parameterList[0])
            }
        }

        return command.parameterList
            .first { p -> p.optionalParam != null }.optionalParam!!
    }

    /**
     * Set the value of the optional parameter with the given key name. If the parameter already exists,
     * its value is changed. If no key with the given name exists yet, a new one is created with the given value.
     *
     * @param name The name of the parameter to change
     * @param value The new parameter value. If the value is null, the parameter will have a key only.
     */
    fun setOptionalParameter(command: LatexCommandWithParams, name: String, value: String?): LatexOptionalKeyValPair? {
        val optionalParam = getOrCreateLabelOptionalParameters(command)

        val parameterText = if (value != null) {
            "$name=$value"
        }
        else {
            name
        }

        val pair = createKeyValuePairs(parameterText)
        val closeBracket = optionalParam.childrenOfType<LeafPsiElement>().firstOrNull { it.elementType == CLOSE_BRACKET }
        return if (optionalParam.optionalKeyValPairList.isNotEmpty()) {
            val existing = optionalParam.optionalKeyValPairList.find { kv -> kv.optionalKeyValKey.text == name }
            if (existing != null && pair.keyValValue != null) {
                existing.keyValValue?.delete()
                existing.addAfter(
                    pair.keyValValue!!,
                    existing.childrenOfType<LeafPsiElement>().firstOrNull { it.elementType == EQUALS }
                )
                existing
            }
            else {
                if (closeBracket?.treeParent != optionalParam.node) {
                    Log.error("Close bracket is not a child of the optional parameter for ${command.text}, name=$name, value=$value")
                }
                val comma = createFromText(",").firstChildOfType(LatexNormalText::class)?.firstChild ?: return pair
                optionalParam.addBefore(comma, closeBracket)
                optionalParam.addBefore(pair, closeBracket)
                closeBracket?.prevSibling as? LatexOptionalKeyValPair
            }
        }
        else {
            optionalParam.addBefore(pair, closeBracket)
            closeBracket?.prevSibling as? LatexOptionalKeyValPair
        }
    }

    /**
     * Create a [PsiWhiteSpace] element that contains the first spacing of the string [space].
     */
    fun createSpacing(space: String = " "): PsiWhiteSpace? = LatexPsiHelper(project)
        .createFromText(space)
        .firstChildOfType(PsiWhiteSpace::class)
}

package nl.hannahsten.texifyidea.psi

import nl.hannahsten.texifyidea.index.stub.LatexCommandsStubElementType
import nl.hannahsten.texifyidea.index.stub.LatexEnvironmentStubElementType
import nl.hannahsten.texifyidea.index.stub.LatexMagicCommentStubElementType

/**
 * Grammar-Kit cannot generate this file containing only stub element types, so we have to maintain it manually.
 */
interface LatexStubElementTypes {

    companion object {

        @JvmField
        val COMMANDS = LatexTypes.COMMANDS as LatexCommandsStubElementType

        @JvmField
        val ENVIRONMENT = LatexTypes.ENVIRONMENT as LatexEnvironmentStubElementType

        @JvmField
        val MAGIC_COMMENT = LatexTypes.MAGIC_COMMENT as LatexMagicCommentStubElementType
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import org.jetbrains.annotations.NonNls

/**
 * @author Sten Wessel
 */
class LatexTokenType(@NonNls debugName: String) : IElementType(debugName, LatexLanguage) {

    override fun toString() = "LatexTokenType." + super.toString()
}

package nl.hannahsten.texifyidea.psi

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiTreeChangeEvent
import com.intellij.psi.PsiTreeChangeListener

/**
 * @author Hannah Schellekens
 */
class StructurePsiChangeListener(val project: Project) : PsiTreeChangeListener {

    private fun updateTracker() {
        PsiManager.getInstance(project).dropPsiCaches()
    }

    override fun beforeChildAddition(psiTreeChangeEvent: PsiTreeChangeEvent) {
        // Do nothing.
    }

    override fun beforeChildRemoval(psiTreeChangeEvent: PsiTreeChangeEvent) {
        // Do nothing.
    }

    override fun beforeChildReplacement(psiTreeChangeEvent: PsiTreeChangeEvent) {
        // Do nothing.
    }

    override fun beforeChildMovement(psiTreeChangeEvent: PsiTreeChangeEvent) {
        // Do nothing.
    }

    override fun beforeChildrenChange(psiTreeChangeEvent: PsiTreeChangeEvent) {
        // Do nothing.
    }

    override fun beforePropertyChange(psiTreeChangeEvent: PsiTreeChangeEvent) {
        // Do nothing.
    }

    override fun childAdded(psiTreeChangeEvent: PsiTreeChangeEvent) {
        updateTracker()
    }

    override fun childRemoved(psiTreeChangeEvent: PsiTreeChangeEvent) {
        updateTracker()
    }

    override fun childReplaced(psiTreeChangeEvent: PsiTreeChangeEvent) {
        updateTracker()
    }

    override fun childrenChanged(psiTreeChangeEvent: PsiTreeChangeEvent) {
        updateTracker()
    }

    override fun childMoved(psiTreeChangeEvent: PsiTreeChangeEvent) {
        updateTracker()
    }

    override fun propertyChanged(psiTreeChangeEvent: PsiTreeChangeEvent) {
        // Do nothing
    }
}

package nl.hannahsten.texifyidea.refactoring

import com.intellij.lang.refactoring.RefactoringSupportProvider
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.BibtexId

/**
 * This class is used to enable inline refactoring.
 */
class BibtexRefactoringSupportProvider : RefactoringSupportProvider() {

    override fun isMemberInplaceRenameAvailable(element: PsiElement, context: PsiElement?): Boolean {
        // Inline refactoring is not enabled for BibtexId because it will not handle the comma separator correctly when the bibtex id is renamed directly (not from usages).
        return false
    }

    override fun isSafeDeleteAvailable(element: PsiElement): Boolean {
        return element is BibtexId
    }
}

package nl.hannahsten.texifyidea.refactoring

import com.intellij.psi.PsiElement
import com.intellij.psi.codeStyle.SuggestedNameInfo
import com.intellij.refactoring.rename.NameSuggestionProvider
import nl.hannahsten.texifyidea.lang.DefaultEnvironment
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.util.parser.firstParentOfType

class LatexNameSuggestionProvider : NameSuggestionProvider {

    override fun getSuggestedNames(element: PsiElement, nameSuggestionContext: PsiElement?, result: MutableSet<String>): SuggestedNameInfo? {
        if (element.firstParentOfType(LatexBeginCommand::class) != null) {
            result.addAll(DefaultEnvironment.values().map { it.environmentName })
        }

        return null
    }
}

package nl.hannahsten.texifyidea.refactoring

import com.intellij.lang.refactoring.NamesValidator
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.util.formatAsLabel

/**
 * Check if user provided names during refactoring are valid.
 */
class LatexNamesValidator : NamesValidator {

    override fun isKeyword(name: String, project: Project?): Boolean {
        return false
    }

    override fun isIdentifier(name: String, project: Project?): Boolean {
        // Unfortunately this is a global rule
        /** See [formatAsLabel] */
        // Exclude the first \ to allow renaming commands
        return name.matches("^\\\\?[^%~#\\\\]+$".toRegex())
    }
}

package nl.hannahsten.texifyidea.refactoring

import com.intellij.lang.refactoring.RefactoringSupportProvider
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.LatexParameterText

/**
 * This class is used to enable inline refactoring.
 */
class LatexRefactoringSupportProvider : RefactoringSupportProvider() {

    override fun isMemberInplaceRenameAvailable(element: PsiElement, context: PsiElement?): Boolean {
        // Label parameters are LatexParameterText
        return when (element) {
            is LatexParameterText -> true
            else -> false
        }
    }

    override fun isSafeDeleteAvailable(element: PsiElement): Boolean {
        return element is LatexParameterText
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.psi.*
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.psi.BibtexId
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.labels.extractLabelName

/**
 * Reference to a bibtex id.
 * The LatexNormalText, for example the `knuth1990` in `\cite{knuth1990}` will resolve to the bibtex id, so the `knuth1990,` in `@Book{knuth1990,`
 */
class BibtexIdReference(element: LatexParameterText) : PsiReferenceBase<LatexParameterText>(element), PsiPolyVariantReference {

    init {
        rangeInElement = ElementManipulators.getValueTextRange(element)
    }

    override fun resolve(): PsiElement? {
        val resolveResults = multiResolve(false)
        return if (resolveResults.size == 1) resolveResults[0].element else null
    }

    override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
        return BibtexEntryIndex().getIndexedEntriesInFileSet(myElement.containingFile)
            .filter { it.extractLabelName() == myElement.name }
            .mapNotNull {
                // Resolve to the id, similarly as why we resolve to the label text for latex labels
                val id = it.firstChildOfType(BibtexId::class) ?: return@mapNotNull null
                PsiElementResolveResult(id)
            }
            .toList()
            .toArray(emptyArray())
    }

    override fun isReferenceTo(element: PsiElement): Boolean {
        return multiResolve(false).any { it.element == element }
    }

    override fun handleElementRename(newElementName: String): PsiElement {
        myElement.setName(newElementName)
        return myElement
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.psi.*
import nl.hannahsten.texifyidea.psi.BibtexDefinedString
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.tags
import nl.hannahsten.texifyidea.util.toTextRange
import nl.hannahsten.texifyidea.util.tokenName
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class BibtexStringReference(
    val string: BibtexDefinedString
) : PsiReferenceBase<BibtexDefinedString>(string), PsiPolyVariantReference {

    init {
        rangeInElement = (0..string.textLength).toTextRange()
    }

    override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
        return string.containingFile.childrenOfType(BibtexEntry::class).asSequence()
            .filter { it.tokenName().lowercase(Locale.getDefault()) == "string" }
            .map { it.tags() }
            .filter { !it.isEmpty() }
            .map { it.first().key }
            .filter { it.text == string.text }
            .map { PsiElementResolveResult(it) }
            .toList()
            .toTypedArray()
    }

    override fun resolve(): PsiElement? {
        val results = multiResolve(false)
        return if (results.size == 1) results[0].element else null
    }

    override fun getVariants() = emptyArray<Any>()
}

package nl.hannahsten.texifyidea.reference

import com.intellij.lang.cacheBuilder.DefaultWordsScanner
import com.intellij.lang.cacheBuilder.WordsScanner
import com.intellij.lang.findUsages.FindUsagesProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.tree.TokenSet
import nl.hannahsten.texifyidea.grammar.BibtexLexerAdapter
import nl.hannahsten.texifyidea.psi.BibtexTypes

class BibtexUsagesProvider : FindUsagesProvider {

    override fun getWordsScanner(): WordsScanner {
        return DefaultWordsScanner(
            BibtexLexerAdapter(),
            // Identifiers.
            TokenSet.create(
                BibtexTypes.ID, BibtexTypes.IDENTIFIER
            ),
            // Comments.
            TokenSet.create(BibtexTypes.COMMENT, BibtexTypes.COMMENT_TOKEN),
            // Literals.
            TokenSet.create(
                BibtexTypes.STRING, BibtexTypes.BRACED_STRING,
                BibtexTypes.QUOTED_STRING, BibtexTypes.DEFINED_STRING,
                BibtexTypes.NORMAL_TEXT_WORD, BibtexTypes.NORMAL_TEXT
            )
        )
    }

    override fun getNodeText(element: PsiElement, useFullName: Boolean): String {
        return element.node.text
    }

    override fun getDescriptiveName(element: PsiElement): String {
        return element.text
    }

    override fun getType(element: PsiElement): String {
        return element.node.elementType.toString()
    }

    override fun getHelpId(psiElement: PsiElement): String? {
        return null
    }

    override fun canFindUsagesFor(psiElement: PsiElement): Boolean {
        return psiElement is PsiNameIdentifierOwner
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.psi.*
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexParameter
import nl.hannahsten.texifyidea.util.parser.definitionCommand
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.parentsOfType
import nl.hannahsten.texifyidea.util.projectSearchScope

/**
 * Command reference. When resolved, points to the command definition.
 *
 * @author Abby Berkers
 */
class CommandDefinitionReference(element: LatexCommands) : PsiReferenceBase<LatexCommands>(element), PsiPolyVariantReference {

    init {
        rangeInElement = ElementManipulators.getValueTextRange(element)
    }

    // Find all command definitions and redefinitions which define the current element
    override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
        val definitionsAndRedefinitions = CommandMagic.commandDefinitionsAndRedefinitions + CommandMagic.commandRedefinitions

        // Don't resolve to a definition when you are in a \newcommand,
        // and if this element is the element that is being defined
        if (element.parentsOfType<LatexCommands>().any { it.name in definitionsAndRedefinitions } &&
            element.parent.firstParentOfType(LatexCommands::class)?.parameterList?.firstOrNull() == element.firstParentOfType(LatexParameter::class)
        ) {
            return emptyArray()
        }
        else {
            return LatexDefinitionIndex.getCommandsByNames(definitionsAndRedefinitions, element.project, element.project.projectSearchScope)
                .filter { it.getRequiredParameters().firstOrNull() == element.name }
                .mapNotNull { newcommand ->
                    // Find the command being defined, e.g. \hi in case of \newcommand{\hi}{}
                    // We should resolve to \hi, not to \newcommand, because otherwise the find usages will try to find references to the \hi definition and won't find anything because the references point to the \newcommand
                    val definedCommand = newcommand.definitionCommand()

                    if (definedCommand == null) {
                        null
                    }
                    else {
                        PsiElementResolveResult(definedCommand as PsiElement)
                    }
                }
                .toArray(emptyArray())
        }
    }

    override fun resolve(): PsiElement? {
        val resolveResults = multiResolve(false)
        return if (resolveResults.size == 1) resolveResults[0].element else null
    }

    // Check if this reference resolves to the given element
    override fun isReferenceTo(element: PsiElement): Boolean {
        return multiResolve(false).any { it.element == element }
    }

    override fun handleElementRename(newElementName: String): PsiElement {
        myElement.setName(newElementName)
        return myElement
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.execution.RunManager
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiReferenceBase
import nl.hannahsten.texifyidea.algorithm.BFS
import nl.hannahsten.texifyidea.completion.pathcompletion.LatexGraphicsPathProvider
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import nl.hannahsten.texifyidea.util.LatexmkRcFileFinder
import nl.hannahsten.texifyidea.util.expandCommandsOnce
import nl.hannahsten.texifyidea.util.files.*
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.isTestProject
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * Reference to a file, based on the command and the range of the filename within the command text.
 *
 * @param defaultExtension Default extension of the command in which this reference is.
 */
class InputFileReference(
    element: LatexCommands,
    val range: TextRange,
    val extensions: Set<String>,
    val defaultExtension: String
) : PsiReferenceBase<LatexCommands>(element) {

    init {
        rangeInElement = range
    }

    companion object {

        /**
         * Handle element rename, but taking into account whether the given
         * newElementName is just a filename which we have to replace,
         * or a full relative path (in which case we replace the whole path).
         */
        fun handleElementRename(command: LatexCommands, newElementName: String, elementNameIsJustFilename: Boolean): PsiElement {
            // A file has been renamed and we are given a new filename, to be replaced in the parameter text of the current command
            // It seems to be problematic to find the old filename we want to replace
            // Since the parameter content may be a path, but we are just given a filename, just replace the filename
            // We guess the filename is after the last occurrence of /
            val oldNode = command.node

            val newName = if ((oldNode?.psi as? LatexCommands)?.name in CommandMagic.illegalExtensions.keys) {
                newElementName.removeFileExtension()
            }
            else {
                newElementName
            }

            val defaultNewText = "${command.name}{$newName}"
            // Assumes that it is the last parameter, but at least leaves the options intact
            val default = oldNode?.text?.replaceAfterLast('{', "$newName}", defaultNewText) ?: defaultNewText

            // Recall that \ is a file separator on Windows
            val newText = if (elementNameIsJustFilename) {
                oldNode?.text?.trimStart('\\')?.replaceAfterLast('/', "$newName}", default.trimStart('\\'))
                    ?.let { "\\" + it } ?: default
            }
            else {
                default
            }
            val newNode = LatexPsiHelper(command.project).createFromText(newText).firstChild.node ?: return command
            if (oldNode == null) {
                command.parent?.node?.addChild(newNode)
            }
            else {
                command.parent.node.replaceChild(oldNode, newNode)
            }
            return command
        }
    }

    val key by lazy {
        rangeInElement.substring(element.text)
    }

    override fun resolve(): PsiFile? {
        return resolve(true)
    }

    /**
     * @param lookForInstalledPackages
     *              Whether to look for packages installed elsewhere on the filesystem.
     *              Set to false when it would make the operation too expensive, for example when trying to
     *              calculate the fileset of many files.
     * @param givenRootFile Used to avoid unnecessarily recalculating the root file.
     * @param isBuildingFileset
     *              True if we are building the fileset.
     *              If false we also need to resolve to graphics files. Doing so is really expensive at
     *              the moment (at least until the implementation in LatexGraphicsPathProvider is improved):
     *              for projects with 500 include commands in hundreds of files this can take 10 seconds in total if
     *              you call this function for every include command.
     *              However, note that doing only one resolve is not too expensive at all
     *              (10 seconds divided by 500 commands/resolves) so this is not a problem when doing only one resolve
     *              (if requested by the user).
     */
    fun resolve(lookForInstalledPackages: Boolean, givenRootFile: VirtualFile? = null, isBuildingFileset: Boolean = false): PsiFile? {
        // IMPORTANT In this method, do not use any functionality which makes use of the file set,
        // because this function is used to find the file set so that would cause an infinite loop

        // Get a list of extra paths to search in for the file, absolute or relative (to the directory containing the root file)
        val searchPaths = mutableListOf<String>()

        // Find the sources root of the current file.
        // findRootFile will also call getImportPaths, so that will be executed twice
        val rootFiles = if (givenRootFile != null) setOf(givenRootFile) else element.containingFile.findRootFiles().mapNotNull { it.virtualFile }
        val rootDirectories = rootFiles.mapNotNull { it.parent }

        // Check environment variables
        val runManager = RunManagerImpl.getInstanceImpl(element.project) as RunManager
        val texInputPath = runManager.allConfigurationsList
            .filterIsInstance<LatexRunConfiguration>()
            .firstOrNull { it.mainFile in rootFiles }
            ?.environmentVariables
            ?.envs
            ?.getOrDefault("TEXINPUTS", null)
            ?: LatexmkRcFileFinder.getTexinputsVariable(element.containingFile, null)

        if (texInputPath != null) {
            val path = texInputPath.trimEnd(':')
            searchPaths.add(path.trimEnd('/'))
            // See the kpathsea manual, // expands to subdirs
            if (path.endsWith("//")) {
                LocalFileSystem.getInstance().findFileByPath(path.trimEnd('/'))?.let { parent ->
                    searchPaths.addAll(
                        parent.allChildDirectories()
                            .filter { it.isDirectory }
                            .map { it.path }
                    )
                }
            }
        }

        // BIBINPUTS
        // Not used for building the fileset, so we can use the fileset to lookup the BIBINPUTS environment variable
        if (!isBuildingFileset && (element.name in CommandMagic.bibliographyIncludeCommands || extensions.contains("bib"))) {
            val bibRunConfigs = element.containingFile.getBibtexRunConfigurations()
            if (bibRunConfigs.any { config -> config.environmentVariables.envs.keys.any { it == "BIBINPUTS" } }) {
                // When using BIBINPUTS, the file will only be sought relative to BIBINPUTS
                searchPaths.clear()
                searchPaths.addAll(bibRunConfigs.mapNotNull { it.environmentVariables.envs["BIBINPUTS"] })
            }
        }

        var processedKey = expandCommandsOnce(key, element.project, file = rootFiles.firstOrNull()?.psiFile(element.project)) ?: key
        // Leading and trailing whitespaces seem to be ignored, at least it holds for \include-like commands
        processedKey = processedKey.trim()

        var targetFile: VirtualFile? = null

        // Try to find the target file directly from the given path
        @Suppress("KotlinConstantConditions")
        if (targetFile == null) {
            for (rootDirectory in rootDirectories) {
                targetFile = rootDirectory.findFile(filePath = processedKey, extensions = extensions)
                if (targetFile != null) break
            }
        }

        // Try content roots
        if (targetFile == null && LatexSdkUtil.isMiktexAvailable) {
            for (moduleRoot in ProjectRootManager.getInstance(element.project).contentSourceRoots) {
                targetFile = moduleRoot.findFile(processedKey, extensions)
                if (targetFile != null) break
            }
        }

        // Try graphicspaths
        if (targetFile == null) {
            // If we are not building the fileset, we can make use of it
            if (!isBuildingFileset && element.containingFile.includedPackages().contains(LatexGenericRegularCommand.GRAPHICSPATH.dependency)) {
                // Add the graphics paths to the search paths
                searchPaths.addAll(LatexGraphicsPathProvider().getGraphicsPathsInFileSet(element.containingFile))
            }
            for (searchPath in searchPaths) {
                val path = if (!searchPath.endsWith("/")) "$searchPath/" else searchPath
                for (rootDirectory in rootDirectories) {
                    targetFile = rootDirectory.findFile(path + processedKey, extensions)
                    if (targetFile != null) break
                }
                if (targetFile != null) break
            }
        }

        // Look for packages/files elsewhere using the kpsewhich command.
        if (targetFile == null && lookForInstalledPackages) {
            targetFile = element.getFileNameWithExtensions(processedKey)
                .mapNotNull { LatexPackageLocationCache.getPackageLocation(it, element.project) }
                .firstNotNullOfOrNull { getExternalFile(it) }
        }

        if (targetFile == null) targetFile = searchFileByImportPaths(element)?.virtualFile

        // \externaldocument uses the .aux file in the output directory, we are only interested in the source file, but it can be anywhere (because no relative path will be given, as in the output directory everything will be on the same level).
        // This does not count for building the file set, because the external document is not actually in the fileset, only the label definitions are
        if (!isBuildingFileset && targetFile == null && element.name == LatexGenericRegularCommand.EXTERNALDOCUMENT.commandWithSlash) {
            targetFile = findAnywhereInProject(processedKey)
        }

        if (targetFile == null) return null

        // Return a reference to the target file.
        return PsiManager.getInstance(element.project).findFile(targetFile)
    }

    /**
     * Try to find the file anywhere in the project. Returns the first match.
     * Might be expensive for large projects because of recursively visiting all directories, not sure.
     */
    fun findAnywhereInProject(fileName: String): VirtualFile? {
        val basePath = if (element.project.isTestProject().not()) {
            LocalFileSystem.getInstance().findFileByPath(element.project.basePath ?: return null) ?: return null
        }
        else {
            element.containingFile.virtualFile.parent ?: return null
        }
        BFS(basePath, { file -> file.children.toList() }).apply {
            iterationAction = { file: VirtualFile ->
                if (file.nameWithoutExtension == fileName && file.extension in extensions) {
                    BFS.BFSAction.ABORT
                }
                else {
                    BFS.BFSAction.CONTINUE
                }
            }
            execute()
            return end
        }
    }

    override fun handleElementRename(newElementName: String): PsiElement {
        return handleElementRename(element, newElementName, true)
    }

    // Required for moving referenced files
    override fun bindToElement(givenElement: PsiElement): PsiElement {
        val newFile = givenElement as? PsiFile ?: return this.element
        // Assume LaTeX will accept paths relative to the root file
        val newFileName = newFile.virtualFile?.path?.toRelativePath(this.element.containingFile.findRootFile().virtualFile.parent.path) ?: return this.element
        return handleElementRename(element, newFileName, false)
    }

    /**
     * Create a set possible complete file names (including extension), based on
     * the command that includes a file, and the name of the file.
     */
    private fun LatexCommands.getFileNameWithExtensions(fileName: String): Set<String> {
        val extension = CommandMagic.includeOnlyExtensions[this.commandToken.text] ?: emptySet()
        return extension.map { "$fileName.$it" }.toSet() + setOf(fileName)
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.psi.ElementManipulators
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReferenceBase
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.firstParentOfType

/**
 * This reference allows refactoring of environments, by letting the text in \end resolve to the text in \begin, so the \begin is viewed as a definition and the text in \end as usage.
 *
 * @author Thomas
 */
class LatexEnvironmentReference(element: LatexParameterText) : PsiReferenceBase<LatexParameterText>(element) {

    init {
        rangeInElement = ElementManipulators.getValueTextRange(element)
    }

    override fun isReferenceTo(element: PsiElement): Boolean {
        return resolve() == element
    }

    override fun resolve(): PsiElement? {
        // Navigate from the current text in \end, to the text in \begin
        return element.firstParentOfType(LatexEnvironment::class)
            ?.firstChildOfType(LatexBeginCommand::class)
            ?.firstChildOfType(LatexParameterText::class)
    }

    override fun handleElementRename(newElementName: String): PsiElement {
        myElement.setName(newElementName)
        return myElement
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.psi.*
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.index.LatexGlossaryEntryIndex
import nl.hannahsten.texifyidea.lang.commands.LatexGlossariesCommand
import nl.hannahsten.texifyidea.psi.LatexParameterText

/**
 * This reference allows refactoring of glossary entries. A glossary reference command (e.g. \gls) references the label
 * parameter of a glossary entry command (e.g. \newglossaryentry).
 */
class LatexGlossaryReference(element: LatexParameterText) :
    PsiReferenceBase<LatexParameterText>(element),
    PsiPolyVariantReference {

    init {
        rangeInElement = ElementManipulators.getValueTextRange(element)
    }

    override fun isReferenceTo(element: PsiElement): Boolean {
        return multiResolve(false).any { it.element == element }
    }

    override fun resolve(): PsiElement? {
        val resolveResults = multiResolve(false)
        return if (resolveResults.size == 1) resolveResults[0].element else null
    }

    override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
        val glossaryEntries = LatexGlossaryEntryIndex.getItemsInFileSet(myElement.containingFile.originalFile)
        return glossaryEntries
            .filter { LatexGlossariesCommand.extractGlossaryLabel(it) == myElement.name }
            .toSet()
            .mapNotNull {
                PsiElementResolveResult(
                    LatexGlossariesCommand.extractGlossaryLabelElement(it) ?: return@mapNotNull null
                )
            }
            .toList()
            .toArray(emptyArray())
    }

    override fun handleElementRename(newElementName: String): PsiElement {
        myElement.setName(newElementName)
        return myElement
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.psi.*
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.labels.extractLabelElement
import nl.hannahsten.texifyidea.util.labels.extractLabelName
import nl.hannahsten.texifyidea.util.labels.findLatexLabelingElementsInFileSet

/**
 * The difference with [LatexLabelReference] is that this reference works on parameter text, i.e. the actual label parameters.
 * This means that the parameter of a \ref command will resolve to the parameter of the \label command.
 *
 * This allows us to implement find usages as well.
 *
 * @author Thomas
 */
class LatexLabelParameterReference(element: LatexParameterText) : PsiReferenceBase<LatexParameterText>(element), PsiPolyVariantReference {

    init {
        rangeInElement = ElementManipulators.getValueTextRange(element)
    }

    override fun isReferenceTo(element: PsiElement): Boolean {
        return multiResolve(false).any { it.element == element }
    }

    override fun resolve(): PsiElement? {
        val resolveResults = multiResolve(false)
        return if (resolveResults.size == 1) resolveResults[0].element else null
    }

    override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
        val allCommands = myElement.containingFile.originalFile.commandsInFileSet()
        // Find the label definition
        return myElement.containingFile.findLatexLabelingElementsInFileSet()
            .filter { it.extractLabelName(referencingFileSetCommands = allCommands) == myElement.name }
            .toSet()
            .mapNotNull {
                // Find the normal text in the label command.
                // We cannot just resolve to the label command itself, because for Find Usages IJ will get the name of the element
                // under the cursor and use the words scanner to look for it (and then check if the elements found are references to the element under the cursor)
                // but only the label text itself will have the correct name for that.
                PsiElementResolveResult(it.extractLabelElement() ?: return@mapNotNull null)
            }
            .toList()
            .toArray(emptyArray())
    }

    override fun handleElementRename(newElementName: String): PsiElement {
        myElement.setName(newElementName)
        return myElement
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReferenceBase
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.completion.handlers.LatexReferenceInsertHandler
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.labels.extractLabelName
import nl.hannahsten.texifyidea.util.labels.findBibtexItems
import nl.hannahsten.texifyidea.util.labels.findLatexLabelingElementsInFileSet
import nl.hannahsten.texifyidea.util.labels.getLabelReferenceCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import java.util.*

/**
 * A reference to a label, used only for autocompletion (together with LatexCommandsImplUtil#extractLabelReferences). For the real referencing, see [LatexLabelParameterReference].
 *
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexLabelReference(element: LatexCommands, range: TextRange?) : PsiReferenceBase<LatexCommands?>(element) {

    override fun resolve(): PsiElement? {
        return null
    }

    override fun getVariants(): Array<Any> {
        val file = myElement!!.containingFile.originalFile
        val command = (myElement as LatexCommands).commandToken.text

        // add bibreferences to autocompletion for \cite-style commands
        if (CommandMagic.bibliographyReference.contains(command)) {
            return file.findBibtexItems().stream()
                .map { bibtexEntry: PsiElement? ->
                    if (bibtexEntry != null) {
                        val containing = bibtexEntry.containingFile
                        if (bibtexEntry is LatexCommands) {
                            val parameters = bibtexEntry.getRequiredParameters()
                            return@map LookupElementBuilder.create(parameters[0])
                                .bold()
                                .withInsertHandler(LatexReferenceInsertHandler())
                                .withTypeText(
                                    containing.name + ": " +
                                        (
                                            1 + StringUtil.offsetToLineNumber(
                                                containing.text,
                                                bibtexEntry.getTextOffset()
                                            )
                                            ),
                                    true
                                )
                                .withIcon(TexifyIcons.DOT_BIB)
                        }
                        else {
                            return@map null
                        }
                    }
                    null
                }.filter { o: LookupElementBuilder? -> Objects.nonNull(o) }.toArray()
        }
        else if (element.project.getLabelReferenceCommands().contains(command)) {
            // Create autocompletion entries for each element we could possibly resolve to
            val allCommands = file.commandsInFileSet()
            return file.findLatexLabelingElementsInFileSet()
                .toSet()
                .mapNotNull { labelingCommand: PsiElement ->
                    val extractedLabel = labelingCommand.extractLabelName(referencingFileSetCommands = allCommands)
                    if (extractedLabel.isBlank()) return@mapNotNull null

                    LookupElementBuilder
                        .create(extractedLabel)
                        .bold()
                        .withInsertHandler(LatexReferenceInsertHandler())
                        .withTypeText(
                            labelingCommand.containingFile.name + ":" +
                                (
                                    1 + StringUtil.offsetToLineNumber(
                                        labelingCommand.containingFile.text,
                                        labelingCommand.textOffset
                                    )
                                    ),
                            true
                        )
                        .withIcon(TexifyIcons.DOT_LABEL)
                }.toList().toTypedArray()
        }
        // if command isn't ref or cite-styled return empty array
        return arrayOf()
    }

    init {

        // Only show Ctrl+click underline under the reference name
        setRangeInElement(range)
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.lang.cacheBuilder.DefaultWordsScanner
import com.intellij.lang.cacheBuilder.WordsScanner
import com.intellij.lang.findUsages.FindUsagesProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.tree.TokenSet
import nl.hannahsten.texifyidea.grammar.LatexLexerAdapter
import nl.hannahsten.texifyidea.psi.LatexTypes

class LatexUsagesProvider : FindUsagesProvider {

    override fun getWordsScanner(): WordsScanner {
        return DefaultWordsScanner(
            LatexLexerAdapter(),
            // Identifiers.
            TokenSet.create(
                LatexTypes.COMMAND_TOKEN, LatexTypes.COMMANDS,
                LatexTypes.BEGIN_COMMAND, LatexTypes.BEGIN_TOKEN,
                LatexTypes.END_COMMAND, LatexTypes.END_TOKEN,
                LatexTypes.PARAMETER_TEXT, LatexTypes.PARAMETER,
                LatexTypes.REQUIRED_PARAM, LatexTypes.OPTIONAL_PARAM
            ),
            // Comments.
            TokenSet.create(LatexTypes.COMMENT_TOKEN, LatexTypes.COMMENT),
            // Literals.
            TokenSet.create(
                LatexTypes.ENVIRONMENT_CONTENT, LatexTypes.CONTENT,
                LatexTypes.MATH_CONTENT, LatexTypes.ENVIRONMENT,
                LatexTypes.MATH_ENVIRONMENT, LatexTypes.DISPLAY_MATH,
                LatexTypes.INLINE_MATH, LatexTypes.NO_MATH_CONTENT,
                LatexTypes.GROUP, LatexTypes.OPTIONAL_PARAM
            )
        )
    }

    override fun getNodeText(element: PsiElement, useFullName: Boolean): String {
        return element.node.text
    }

    override fun getDescriptiveName(element: PsiElement): String {
        return element.text
    }

    override fun getType(element: PsiElement): String {
        return element.node.elementType.toString()
    }

    override fun getHelpId(psiElement: PsiElement): String? {
        return null
    }

    override fun canFindUsagesFor(psiElement: PsiElement): Boolean {
        return psiElement is PsiNameIdentifierOwner
    }
}

package nl.hannahsten.texifyidea.reference

import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiReferenceBase
import nl.hannahsten.texifyidea.util.files.findVirtualFileByAbsoluteOrRelativePath
import nl.hannahsten.texifyidea.util.files.psiFile

/**
 * Assuming the given PsiElement contains the filePath (absolute or relative), this is a reference to the file.
 * If textRange is given that will be used, otherwise the range of filePath will be used.
 */
class SimpleFileReference(fileReference: PsiElement, val filePath: String, val textRange: TextRange? = null) : PsiReferenceBase<PsiElement>(fileReference) {

    init {
        rangeInElement = textRange ?: TextRange.from(element.text.indexOf(filePath), filePath.length)
    }

    override fun resolve(): PsiFile? {
        return element.containingFile
            ?.containingDirectory
            ?.virtualFile
            ?.findVirtualFileByAbsoluteOrRelativePath(filePath)
            ?.psiFile(element.project)
    }
}

package nl.hannahsten.texifyidea.remotelibraries

import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import io.ktor.client.statement.*
import nl.hannahsten.texifyidea.RemoteLibraryRequestFailure
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.remotelibraries.state.BibtexEntryListConverter

/**
 * Remote library with a unique [identifier].
 */
abstract class RemoteBibLibrary(open val identifier: String, open val displayName: String) {

    /**
     * Get the bib items from the remote library in bibtex format, then parse the bibtex to obtain all the bib entries.
     *
     * When the request has a non-OK status code, return a failure instead.
     */
    suspend fun getCollection(): Either<RemoteLibraryRequestFailure, List<BibtexEntry>> = either {
        val (response, body) = getBibtexString()

        ensure(response.status.value !in 200 until 300) {
            RemoteLibraryRequestFailure(displayName, response)
        }

        // Reading the dummy bib file needs to happen in a place where we have read access.
        runReadAction {
            BibtexEntryListConverter().fromString(body)
        }
    }

    fun showNotification(project: Project, libraryName: String, response: HttpResponse) {
        val title = "Could not connect to $libraryName"
        val statusMessage = "${response.status.value}: ${response.status.description}"
        Notification("LaTeX", title, statusMessage, NotificationType.ERROR).notify(project)
    }

    /**
     * Get the bib items from the remote library in bibtex format.
     */
    abstract suspend fun getBibtexString(): Pair<HttpResponse, String>

    /**
     * Remove any credentials from the password safe.
     *
     * Use `PasswordSafe.instance.set(key, null)` to remove credentials for `key` from the password safe.
     */
    abstract fun destroyCredentials()
}

package nl.hannahsten.texifyidea.remotelibraries

import nl.hannahsten.texifyidea.remotelibraries.mendeley.MendeleyLibrary
import nl.hannahsten.texifyidea.remotelibraries.zotero.ZoteroLibrary
import java.nio.charset.StandardCharsets.UTF_8
import java.security.MessageDigest
import kotlin.random.Random

/**
 * Factory to create remote bib libraries. There is one library for all libraries, because they have to use the same id
 * generator, and all libraries should be able to be constructed from the same information (the id, display name, and optionally
 * sensitive information that is stored in the password safe).
 */
object RemoteBibLibraryFactory {

    /**
     * Create a remote library from the unique identifier and the display name. Reads from the password safe if needed.
     */
    fun fromStorage(identifier: String?): RemoteBibLibrary? {
        val libraryState = RemoteLibraryManager.getInstance().getLibraries()[identifier ?: return null] ?: return null
        return when (libraryState.libraryType.simpleName) {
            ZoteroLibrary::class.simpleName -> ZoteroLibrary(identifier, libraryState.displayName)
            MendeleyLibrary::class.simpleName -> MendeleyLibrary(identifier, libraryState.displayName)
            else -> null
        }
    }

    /**
     * Create a new library with a random unique id. This doesn't use the password safe.
     */
    inline fun <reified T : RemoteBibLibrary> create(displayName: String): T? {
        val identifier = generateId()

        return when (T::class.simpleName) {
            ZoteroLibrary::class.simpleName -> ZoteroLibrary(identifier, displayName) as T
            MendeleyLibrary::class.simpleName -> MendeleyLibrary(identifier, displayName) as T
            else -> null
        }
    }

    /**
     * Generate a random unique id by hashing some integer.
     */
    fun generateId(): String {
        fun sha(str: String): ByteArray = MessageDigest.getInstance("SHA-256").digest(str.toByteArray(UTF_8))
        fun ByteArray.toHex() = joinToString(separator = "") { byte -> "%02x".format(byte) }

        // Prepend an a because an xml-tag cannot start with a number.
        fun candidate() = "a" + sha(Random.nextInt(100, 10000).toString()).toHex()

        var cand = candidate()
        while (cand in RemoteLibraryManager.getInstance().getLibraries().keys) {
            cand = candidate()
        }

        return cand
    }
}

package nl.hannahsten.texifyidea.remotelibraries

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.remotelibraries.state.LibraryState
import nl.hannahsten.texifyidea.remotelibraries.state.RemoteLibraryState

@State(name = "ExternalBibLibraryManager", storages = [(Storage("library.xml"))])
class RemoteLibraryManager : PersistentStateComponent<RemoteLibraryState> {

    companion object {

        fun getInstance(): RemoteLibraryManager = ApplicationManager.getApplication().getService(RemoteLibraryManager::class.java)
    }

    private var libraries: MutableMap<String, LibraryState> = mutableMapOf()

    fun getLibraries(): Map<String, LibraryState> = libraries.toMap()

    override fun getState(): RemoteLibraryState {
        return RemoteLibraryState(libraries.toMap())
    }

    override fun loadState(state: RemoteLibraryState) {
        libraries = state.libraries.toMutableMap()
    }

    /**
     * Update the stored bib entries if the library already has an entry, otherwise create a new entry.
     */
    fun updateLibrary(library: RemoteBibLibrary, bibItems: List<BibtexEntry>) {
        libraries[library.identifier]?.let {
            it.entries = bibItems
        } ?: run {
            libraries[library.identifier] = LibraryState(library.displayName, library::class.java, bibItems)
        }
    }

    fun removeLibrary(library: RemoteBibLibrary) {
        removeLibraryByKey(library.identifier)
    }

    fun removeLibraryByKey(key: String) {
        libraries.remove(key)
    }
}

package nl.hannahsten.texifyidea.run

import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.LocalFileSystem
import nl.hannahsten.texifyidea.util.runWriteAction
import java.io.File

/**
 * Clean up given files after the process is done.
 *
 * @param filesToCleanUp Delete these files.
 * @param filesToCleanUpIfEmpty Delete these files only if they are an empty directory.
 */
class FileCleanupListener(private val filesToCleanUp: MutableList<File>, private val filesToCleanUpIfEmpty: Set<File> = setOf()) : ProcessListener {

    override fun startNotified(event: ProcessEvent) {
    }

    override fun processTerminated(event: ProcessEvent) {
        for (originalFile in filesToCleanUp) {
            val file = LocalFileSystem.getInstance().refreshAndFindFileByPath(originalFile.absolutePath) ?: continue
            runInEdt {
                runWriteAction {
                    file.delete(this)
                }
            }
        }
        filesToCleanUp.clear()

        // Make sure to delete children first, so that we also delete directories containing only directories
        for (file in filesToCleanUpIfEmpty.sortedDescending()) {
            if (file.isDirectory && file.list()?.isEmpty() == true) {
                file.delete()
            }
        }
    }

    override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
    }
}

package nl.hannahsten.texifyidea.run

import com.intellij.execution.RunManager
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.impl.RunConfigurationBeforeRunProvider
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.execution.runners.ExecutionEnvironmentBuilder
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.project.Project
import com.intellij.task.ModuleBuildTask
import com.intellij.task.ProjectTask
import com.intellij.task.ProjectTaskContext
import com.intellij.task.ProjectTaskRunner
import nl.hannahsten.texifyidea.modules.LatexModuleType
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.util.getLatexRunConfigurations
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.Promise

/**
 * This implements the 'build project' button, to the left of the run config dropdown.
 * It's not completely clear what it should be doing, but I think in other languages it just builds
 * the complete project, so I think the most sensible is to run all run configurations.
 */
class LatexProjectTaskRunner : ProjectTaskRunner() {

    override fun canRun(projectTask: ProjectTask): Boolean {
        // Only run if we're only going to build LaTeX run configurations: don't interfere with other languages (e.g. maven tasks)
        return projectTask is ModuleBuildTask && (ModuleType.get(projectTask.module).id == LatexModuleType.ID || RunManager.getInstance(projectTask.module.project).allConfigurationsList.all { it is LatexRunConfiguration })
    }

    override fun run(project: Project, context: ProjectTaskContext, vararg tasks: ProjectTask?): Promise<Result> {
        // Each task will probably be a module, but we don't use it

        project.getLatexRunConfigurations().forEach { runConfig ->
            val latexSettings = RunManagerImpl.getInstanceImpl(project).getSettings(runConfig) ?: return@forEach
            val environment = ExecutionEnvironmentBuilder.createOrNull(DefaultRunExecutor.getRunExecutorInstance(), latexSettings)?.build()
                ?: return@forEach
            RunConfigurationBeforeRunProvider.doExecuteTask(environment, latexSettings, null)
        }

        // Not sure what we're suppose to return
        val promise = AsyncPromise<Result>()
        // TaskRunnerResults.SUCCESS is experimental, we cannot use it
        promise.setResult(object : Result {
            override fun isAborted() = false
            override fun hasErrors() = false
        })
        return promise
    }
}

package nl.hannahsten.texifyidea.run

import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.util.Key
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import java.io.IOException

/**
 * Run the given command after the process has succeeded.
 */
class OpenCustomPdfViewerListener(val command: Array<String>, val failSilently: Boolean = false, val runConfig: LatexRunConfiguration) : ProcessListener {

    override fun processTerminated(event: ProcessEvent) {
        if (event.exitCode == 0) {
            try {
                ProcessBuilder(*command).start()
            }
            catch (e: IOException) {
                if (!failSilently) {
                    // Probably user error
                    Notification("LaTeX", "Could not open pdf file", "An error occured when trying to open the pdf using ${command.joinToString(" ")} with message ${e.message}", NotificationType.ERROR).notify(runConfig.project)
                }
            }
        }

        // Reset to default
        runConfig.allowFocusChange = true
    }

    override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) {
    }

    override fun startNotified(event: ProcessEvent) {
    }

    override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
    }
}

package nl.hannahsten.texifyidea.settings

import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.RawCommandLineEditor
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBLabel
import java.awt.Dimension
import java.awt.FlowLayout
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel

/**
 * @author Hannah Schellekens, Sten Wessel
 */
@Suppress("SameParameterValue")
class TexifyConfigurable : SearchableConfigurable {

    private val settings: TexifySettings = TexifySettings.getInstance()

    private var automaticSecondInlineMathSymbol: JBCheckBox? = null
    private var automaticUpDownBracket: JBCheckBox? = null
    private var automaticItemInItemize: JBCheckBox? = null
    private var automaticDependencyCheck: JBCheckBox? = null
    private var autoCompile: JBCheckBox? = null
    private var autoCompileOnSaveOnly: JBCheckBox? = null
    private var continuousPreview: JBCheckBox? = null
    private var includeBackslashInSelection: JBCheckBox? = null
    private var showPackagesInStructureView: JBCheckBox? = null
    private var enableExternalIndex: JBCheckBox? = null
    private var enableTextidote: JBCheckBox? = null
    private var textidoteOptions: RawCommandLineEditor? = null
    private var latexIndentOptions: RawCommandLineEditor? = null
    private var automaticQuoteReplacement: ComboBox<String>? = null

    /**
     * Map UI variables to underlying setting variables
     */
    private val booleanSettings = listOf(
        Pair(::automaticSecondInlineMathSymbol, settings::automaticSecondInlineMathSymbol),
        Pair(::automaticUpDownBracket, settings::automaticUpDownBracket),
        Pair(::automaticItemInItemize, settings::automaticItemInItemize),
        Pair(::automaticDependencyCheck, settings::automaticDependencyCheck),
        Pair(::autoCompile, settings::autoCompile),
        Pair(::autoCompileOnSaveOnly, settings::autoCompileOnSaveOnly),
        Pair(::continuousPreview, settings::continuousPreview),
        Pair(::includeBackslashInSelection, settings::includeBackslashInSelection),
        Pair(::showPackagesInStructureView, settings::showPackagesInStructureView),
        Pair(::enableExternalIndex, settings::enableExternalIndex),
        Pair(::enableTextidote, settings::enableTextidote),
    )

    override fun getId() = "TexifyConfigurable"

    override fun getDisplayName() = "TeXiFy"

    override fun createComponent(): JComponent {
        return JPanel(FlowLayout(FlowLayout.LEFT)).apply {
            add(
                JPanel().apply {
                    layout = BoxLayout(this, BoxLayout.Y_AXIS)

                    automaticSecondInlineMathSymbol = addCheckbox("Automatically insert second '$'")
                    automaticUpDownBracket = addCheckbox("Automatically insert braces around text in subscript and superscript")
                    automaticItemInItemize = addCheckbox("Automatically insert '\\item' in itemize-like environments on pressing enter")
                    automaticDependencyCheck = addCheckbox("Automatically check for required package dependencies and insert them")
                    autoCompile = addCheckbox("Automatic compilation (warning: can cause high CPU usage)")
                    autoCompileOnSaveOnly = addCheckbox("Automatic compilation only after document save")
                    continuousPreview = addCheckbox("Automatically refresh preview of math and TikZ pictures")
                    includeBackslashInSelection = addCheckbox("Include the backslash in the selection when selecting a LaTeX command")
                    showPackagesInStructureView = addCheckbox("Show LaTeX package files in structure view (warning: structure view will take more time to load)")
                    enableExternalIndex = addCheckbox("Enable indexing of MiKTeX/TeX Live package files (requires restart)")
                    enableTextidote = addCheckbox("Enable the Textidote linter")
                    textidoteOptions = addCommandLineEditor("Textidote", TexifySettingsState().textidoteOptions)
                    latexIndentOptions = addCommandLineEditor("Latexindent", TexifySettingsState().latexIndentOptions)
                    automaticQuoteReplacement = addSmartQuotesOptions("Off", "TeX ligatures", "TeX commands", "csquotes")
                    addPdfViewerText()
                }
            )
        }
    }

    /**
     * Add the options for the smart quote substitution.
     */
    private fun JPanel.addSmartQuotesOptions(vararg values: String): ComboBox<String> {
        val list = ComboBox(values)
        add(
            JPanel(FlowLayout(FlowLayout.LEFT)).apply {
                add(JBLabel("Smart quote substitution: "))
                add(list)
            }
        )
        return list
    }

    /**
     * Add a field for command options
     *
     * @param label Name of the command
     * @param initialValue Only used to guess a good field width
     */
    private fun JPanel.addCommandLineEditor(label: String, initialValue: String): RawCommandLineEditor {
        val cmdEditor = RawCommandLineEditor()

        // It's magic
        val width = cmdEditor.getFontMetrics(cmdEditor.font).stringWidth(initialValue) + 170
        cmdEditor.minimumSize = Dimension(width, cmdEditor.preferredSize.height)
        cmdEditor.size = Dimension(width, cmdEditor.preferredSize.height)
        cmdEditor.preferredSize = Dimension(width, cmdEditor.preferredSize.height)

        add(
            JPanel(FlowLayout(FlowLayout.LEFT)).apply {
                add(JBLabel("$label command line options: "))
                add(cmdEditor)
            }
        )
        return cmdEditor
    }

    private fun JPanel.addPdfViewerText() {
        val oldPdfViewer = TexifySettings.getInstance().pdfViewer
        add(
            JPanel(FlowLayout(FlowLayout.LEFT)).apply {
                add(JLabel("PDF viewer: This setting has been moved to the run configuration (template). See the wiki for details.
Old PDF viewer: $oldPdfViewer
 See for example (IntelliJ) File > New Projects Settings > Run Configuration Templates, or Edit Configurations > Templates > LaTeX for the current project."))
            }
        )
    }

    private fun JPanel.addCheckbox(message: String): JBCheckBox {
        val checkBox = JBCheckBox(message)
        add(
            JPanel(FlowLayout(FlowLayout.LEFT)).apply {
                add(checkBox)
            }
        )
        return checkBox
    }

    override fun isModified(): Boolean {
        return booleanSettings.any { it.first.get()?.isSelected != it.second.get() } ||
            textidoteOptions?.text != settings.textidoteOptions ||
            latexIndentOptions?.text != settings.latexIndentOptions ||
            automaticQuoteReplacement?.selectedIndex != settings.automaticQuoteReplacement.ordinal
    }

    override fun apply() {
        for (setting in booleanSettings) {
            setting.second.set(setting.first.get()?.isSelected == true)
        }
        settings.textidoteOptions = textidoteOptions?.text ?: ""
        settings.latexIndentOptions = latexIndentOptions?.text ?: ""
        settings.automaticQuoteReplacement = TexifySettings.QuoteReplacement.values()[automaticQuoteReplacement?.selectedIndex ?: 0]
    }

    override fun reset() {
        for (setting in booleanSettings) {
            setting.first.get()?.isSelected = setting.second.get()
        }
        textidoteOptions?.text = settings.textidoteOptions
        latexIndentOptions?.text = settings.latexIndentOptions
        automaticQuoteReplacement?.selectedIndex = settings.automaticQuoteReplacement.ordinal
    }
}

package nl.hannahsten.texifyidea.settings

import com.intellij.openapi.options.SearchableConfigurable
import javax.swing.JComponent

/**
 * Project-level settings UI.
 *
 * The project settings are a property so they will differ per project.
 *
 * Note: We don't currently use this, but keeping it so we don't have to figure it out all over again
 */
class TexifyProjectConfigurable : SearchableConfigurable {

    private val projectSettings: TexifyProjectSettings = TexifyProjectSettings()

    override fun getId() = "TexifyProjectConfigurable"

    override fun getDisplayName() = "Project Settings"

    override fun createComponent(): JComponent? {
        return null
    }

    override fun isModified(): Boolean {
        return false
    }

    override fun apply() {}
}

package nl.hannahsten.texifyidea.settings

import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.XmlSerializerUtil

/**
 * Project-level settings are stored in the workspace file.
 * For more information, see https://www.jetbrains.org/intellij/sdk/docs/basics/persisting_state_of_components.html
 */
@State(name = "TexifyProjectSettings", storages = [(Storage(StoragePathMacros.WORKSPACE_FILE))])
class TexifyProjectSettings : PersistentStateComponent<TexifyProjectSettings> {

    companion object {

        @JvmStatic
        fun getInstance(project: Project): TexifyProjectSettings = project.getService(TexifyProjectSettings::class.java)
    }

    override fun getState() = this

    override fun loadState(state: TexifyProjectSettings) {
        XmlSerializerUtil.copyBean(state, this)
    }
}

package nl.hannahsten.texifyidea.settings

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer

/**
 * @author Sten Wessel
 */
@State(name = "TexifySettings", storages = [(Storage("texifySettings.xml"))])
class TexifySettings : PersistentStateComponent<TexifySettingsState> {

    companion object {

        /**
         * Warning: don't retrieve the settings on class initialization (e.g. storing it in a companion object), as that is not unlikely to throw a ProcessCanceledException.
         */
        @JvmStatic
        fun getInstance(): TexifySettings = ApplicationManager.getApplication().getService(TexifySettings::class.java)
    }

    // Options for smart quote replacement, in the order as they appear in the combobox
    enum class QuoteReplacement {

        NONE,
        LIGATURES,
        COMMANDS,
        CSQUOTES // Context Sensitive quotes from the csquotes package
    }

    var automaticSecondInlineMathSymbol = true
    var automaticUpDownBracket = true
    var automaticItemInItemize = true
    var automaticDependencyCheck = true
    var autoCompile = false
    var autoCompileOnSaveOnly = false
    var continuousPreview = false
    var includeBackslashInSelection = false
    var showPackagesInStructureView = false
    var enableExternalIndex = true
    var enableTextidote = false
    var textidoteOptions = "--check en --output singleline --no-color"
    var latexIndentOptions = ""
    var automaticQuoteReplacement = QuoteReplacement.NONE

    /**
     * Backwards compatibility. This value is never altered, only read from/to memory.
     *
     * We keep it here so that when the user migrates from when the pdf viewer was set in TeXiFy settings to when it is
     * set in the run config, we can recover their old setting.
     */
    var pdfViewer = InternalPdfViewer.firstAvailable()

    override fun getState(): TexifySettingsState {
        return TexifySettingsState(
            automaticSecondInlineMathSymbol = automaticSecondInlineMathSymbol,
            automaticUpDownBracket = automaticUpDownBracket,
            automaticItemInItemize = automaticItemInItemize,
            automaticDependencyCheck = automaticDependencyCheck,
            autoCompile = autoCompile,
            autoCompileOnSaveOnly = autoCompileOnSaveOnly,
            continuousPreview = continuousPreview,
            includeBackslashInSelection = includeBackslashInSelection,
            showPackagesInStructureView = showPackagesInStructureView,
            enableExternalIndex = enableExternalIndex,
            enableTextidote = enableTextidote,
            textidoteOptions = textidoteOptions,
            latexIndentOptions = latexIndentOptions,
            automaticQuoteReplacement = automaticQuoteReplacement,
            pdfViewer = pdfViewer
        )
    }

    override fun loadState(state: TexifySettingsState) {
        automaticSecondInlineMathSymbol = state.automaticSecondInlineMathSymbol
        automaticUpDownBracket = state.automaticUpDownBracket
        automaticItemInItemize = state.automaticItemInItemize
        automaticDependencyCheck = state.automaticDependencyCheck
        autoCompile = state.autoCompile
        autoCompileOnSaveOnly = state.autoCompileOnSaveOnly
        continuousPreview = state.continuousPreview
        includeBackslashInSelection = state.includeBackslashInSelection
        showPackagesInStructureView = state.showPackagesInStructureView
        enableExternalIndex = state.enableExternalIndex
        enableTextidote = state.enableTextidote
        textidoteOptions = state.textidoteOptions
        latexIndentOptions = state.latexIndentOptions
        automaticQuoteReplacement = state.automaticQuoteReplacement
        pdfViewer = state.pdfViewer
    }
}

package nl.hannahsten.texifyidea.settings

import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer

data class TexifySettingsState(
    var automaticSecondInlineMathSymbol: Boolean = true,
    var automaticUpDownBracket: Boolean = true,
    var automaticItemInItemize: Boolean = true,
    var automaticDependencyCheck: Boolean = true,
    var autoCompile: Boolean = false,
    var autoCompileOnSaveOnly: Boolean = false,
    var continuousPreview: Boolean = false,
    var includeBackslashInSelection: Boolean = false,
    var showPackagesInStructureView: Boolean = false,
    var enableExternalIndex: Boolean = true,
    var enableTextidote: Boolean = false,
    var textidoteOptions: String = "--check en --output singleline --no-color",
    var latexIndentOptions: String = "",
    var automaticQuoteReplacement: TexifySettings.QuoteReplacement = TexifySettings.QuoteReplacement.NONE,
    var missingLabelMinimumLevel: LatexCommand = LatexGenericRegularCommand.SUBSECTION,
    var pdfViewer: InternalPdfViewer = InternalPdfViewer.firstAvailable()
)

package nl.hannahsten.texifyidea.startup

import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import nl.hannahsten.texifyidea.util.files.LatexPackageLocationCache

/**
 * Initialize package location cache, because filling it takes a long time, we do not want to do that only at the moment we need it (when resolving references).
 */
class LatexPackageLocationCacheInitializer : ProjectActivity {

    override suspend fun execute(project: Project) {
        // Not sure on which thread this is run, run in background to be sure
        CoroutineScope(Dispatchers.Default).launch {
            LatexPackageLocationCache.fillCacheWithKpsewhich(project)
        }
    }
}

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.run.linuxpdfviewer.evince.EvinceInverseSearchListener

class LoadUnloadListener : DynamicPluginListener {

    override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) {
        // Apparently it's not needed to unload these anymore? Were a problem in the past.
//        ControlTracker.unload()
//        ShiftTracker.unload()
        runBlocking { EvinceInverseSearchListener.unload() }
        super.beforePluginUnload(pluginDescriptor, isUpdate)
    }
}

package nl.hannahsten.texifyidea.startup

import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer
import nl.hannahsten.texifyidea.run.linuxpdfviewer.evince.EvinceInverseSearchListener
import nl.hannahsten.texifyidea.util.selectedRunConfig

/**
 * @author Sten Wessel
 */
class StartEvinceInverseSearchListener : ProjectActivity, DumbAware {

    override suspend fun execute(project: Project) {
        if (project.selectedRunConfig()?.pdfViewer == InternalPdfViewer.EVINCE) {
            EvinceInverseSearchListener.start(project)
        }
    }
}

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.runCommand

class TexLivePackageListInitializer : ProjectActivity {

    override suspend fun execute(project: Project) {
        if (TexliveSdk.isAvailable) {
            val result = "tlmgr list --only-installed".runCommand() ?: return
            TexLivePackages.packageList = Regex("i\\s(.*):").findAll(result)
                .map { it.groupValues.last() }.toMutableList()
        }
    }
}

package nl.hannahsten.texifyidea.structure

import com.intellij.navigation.ItemPresentation

/**
 * @author Hannah Schellekens
 */
interface EditableHintPresentation : ItemPresentation {

    fun setHint(hint: String)
}

package nl.hannahsten.texifyidea.templates

import com.intellij.codeInsight.template.TemplateActionContext
import com.intellij.codeInsight.template.TemplateContextType
import nl.hannahsten.texifyidea.file.BibtexFile

/**
 * @author Hannah Schellekens
 */
open class BibtexContext : TemplateContextType("BibTeX") {

    override fun isInContext(context: TemplateActionContext) = context.file is BibtexFile
}

package nl.hannahsten.texifyidea.templates

import com.intellij.codeInsight.template.TemplateActionContext
import com.intellij.codeInsight.template.TemplateContextType
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.parser.inVerbatim

/**
 * Defines a LaTeX template context, used to define in which context
 * a live template is available.
 *
 * @author Abby Berkers
 */
open class LatexContext(name: String) : TemplateContextType(name) {

    override fun isInContext(context: TemplateActionContext): Boolean = context.file is LatexFile

    class Generic : LatexContext("LaTeX") {

        override fun isInContext(context: TemplateActionContext): Boolean =
            context.file is LatexFile && context.file.findElementAt(context.startOffset)?.inVerbatim() == false
    }

    open class LatexMathContext : LatexContext("Math") {

        override fun isInContext(context: TemplateActionContext): Boolean =
            context.file is LatexFile && context.file.findElementAt(context.startOffset)?.inMathContext() == true
    }
}

package nl.hannahsten.texifyidea.templates

import com.intellij.ide.fileTemplates.FileTemplateDescriptor
import com.intellij.ide.fileTemplates.FileTemplateGroupDescriptor
import com.intellij.ide.fileTemplates.FileTemplateGroupDescriptorFactory
import com.intellij.ide.fileTemplates.FileTemplateManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.util.Container
import java.io.IOException
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexTemplatesFactory : FileTemplateGroupDescriptorFactory {

    companion object {

        const val descriptor = "LaTeX"
        const val fileTemplateTex = "LaTeX Source.tex"
        const val fileTemplateTexWithBib = "LaTeX Source With BibTeX.tex"
        const val fileTemplateSty = "LaTeX Package.sty"
        const val fileTemplateCls = "LaTeX Document class.cls"
        const val fileTemplateBib = "BibTeX Bibliography.bib"
        const val fileTemplateTikz = "TikZ Picture.tikz"

        @JvmStatic
        fun createFromTemplate(
            directory: PsiDirectory, fileName: String,
            templateName: String, fileType: FileType
        ): PsiFile {
            val project = directory.project
            val templateText = getTemplateText(project, templateName, fileName)

            val fileFactory = PsiFileFactory.getInstance(project)
            val file = fileFactory.createFileFromText(fileName, fileType, templateText)

            val createdFile = Container<PsiFile>()
            val application = ApplicationManager.getApplication()
            application.runWriteAction { createdFile.item = directory.add(file) as PsiFile }

            return createdFile.item ?: throw TeXception("No created file in container.")
        }

        /**
         * Get the text from a certain template.
         *
         * @param project
         * The IntelliJ project that contains the templates.
         * @param templateName
         * The name of the template. Use the constants prefixed with `FILE_TEMPLATE_` from
         * [LatexTemplatesFactory].
         * @return The contents of the template with applied properties.
         */
        @JvmStatic
        fun getTemplateText(project: Project, templateName: String, fileName: String): String {
            val templateManager = FileTemplateManager.getInstance(project)
            val template = templateManager.getInternalTemplate(templateName)
            val properties = Properties(templateManager.defaultProperties)
            properties["FILE_NAME"] = fileName

            try {
                return template.getText(properties)
            }
            catch (e: IOException) {
                throw TeXception("Could not load template $templateName", e)
            }
        }
    }

    override fun getFileTemplatesDescriptor(): FileTemplateGroupDescriptor {
        val descriptor = FileTemplateGroupDescriptor(
            descriptor,
            TexifyIcons.LATEX_FILE
        )

        descriptor.addTemplate(FileTemplateDescriptor(fileTemplateTex, TexifyIcons.LATEX_FILE))
        descriptor.addTemplate(FileTemplateDescriptor(fileTemplateTexWithBib, TexifyIcons.LATEX_FILE))
        descriptor.addTemplate(FileTemplateDescriptor(fileTemplateSty, TexifyIcons.STYLE_FILE))
        descriptor.addTemplate(FileTemplateDescriptor(fileTemplateCls, TexifyIcons.CLASS_FILE))
        descriptor.addTemplate(FileTemplateDescriptor(fileTemplateBib, TexifyIcons.BIBLIOGRAPHY_FILE))
        descriptor.addTemplate(FileTemplateDescriptor(fileTemplateTikz, TexifyIcons.TIKZ_FILE))

        return descriptor
    }
}

package nl.hannahsten.texifyidea.ui

import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.ui.*
import nl.hannahsten.texifyidea.util.formatAsFileName
import java.io.File
import javax.swing.JPanel
import javax.swing.JTextField

/**
 * @author Abby Berkers
 *
 * Dialog to ask the user for a file name and location when creating a new file.
 *
 * @param currentFilePath The path of the file we are currently in, e.g., the file from which an intention was triggered
 *      that creates a new file.
 * @param newFileName The name of the new file, with or without the extension. Will be reformatted before being shown to the user.
 * @param newFileFullPath The full path of the new file, without tex extension.
 */
class CreateFileDialog(private val currentFilePath: String?, private val newFileName: String, var newFileFullPath: String? = null) {

    init {
        DialogBuilder().apply {
            setTitle("Create New File")
            val panel = JPanel()
            panel.layout = VerticalFlowLayout(VerticalFlowLayout.TOP)

            // Field to enter the name of the new file.
            val nameField = JTextField(newFileName.formatAsFileName())
            // Field to select the folder/location of the new file.
            val pathField = TextFieldWithBrowseButton()
            pathField.text = currentFilePath ?: return@apply
            // Make sure the dialog is wide enough to fit the whole path in the text field.
            pathField.textField.columns = pathField.text.length

            // Add a listener to the browse button to browse a folder
            pathField.addBrowseFolderListener(
                TextBrowseFolderListener(
                    FileChooserDescriptor(false, true, false, false, false, false)
                        .withTitle("Select Folder of New File")
                )
            )

            // Add the fields to the panel, with a useful label.
            panel.add(LabeledComponent.create(nameField, "File path (extension optional, relative to base directory)"))
            panel.add(LabeledComponent.create(pathField, "Base directory"))

            setCenterPanel(panel)
            addCancelAction()
            addOkAction()
            setOkOperation {
                dialogWrapper.close(0)
            }

            // Focus the filename field.
            setPreferredFocusComponent(nameField)

            if (show() == DialogWrapper.OK_EXIT_CODE) {
                val pathEndIndex = nameField.text.lastIndexOf('/')
                // If there is no '/' in the file name, we can use the path from the path field box.
                val path = if (pathEndIndex == -1) {
                    pathField.text
                }
                // If there is a '/' in the file name, we have to append the path with the folders given in the file name.
                else {
                    pathField.text + "/" + nameField.text.substring(0, pathEndIndex)
                }

                // Create the directories, if they do not yet exist.
                File(path).mkdirs()
                // Format the text from the name field as a file name (e.g. " " -> "-") and remove the (double) tex extension.
                // We do not check the filename here, as we already suggested a valid name so if the user decides to change it, it's not our problem.
                newFileFullPath = "$path/${nameField.text.substring(pathEndIndex + 1)}"
                    .replace(Regex("(\\.tex)+$", RegexOption.IGNORE_CASE), "")
            }
        }
    }
}

package nl.hannahsten.texifyidea.ui

import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.content.ContentFactory
import com.intellij.ui.jcef.JBCefBrowser
import nl.hannahsten.texifyidea.util.isLatexProject

class DetexifyToolWindowFactory : ToolWindowFactory {

    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        val detexifyToolWindow = DetexifyToolWindow()
        val content = ContentFactory.getInstance().createContent(detexifyToolWindow.content, "", false)
        toolWindow.contentManager.addContent(content)
    }

    // Non-idea has no concept of modules so we need to use some other criterion based on the project
    override fun isApplicable(project: Project) = project.isLatexProject()

    class DetexifyToolWindow {

        val content = JBCefBrowser(DETEXIFY_URL).component

        companion object {

            const val DETEXIFY_URL = "https://detexify.kirelabs.org/classify.html"
        }
    }
}

package nl.hannahsten.texifyidea.ui

import java.awt.Dimension
import java.awt.Graphics
import java.awt.Image
import java.awt.image.BufferedImage
import java.lang.Double.min
import java.lang.Integer.max
import javax.swing.JPanel

/**
 * @author Sergei Izmailov
 */
internal class ImagePanel : JPanel() {

    companion object {

        private const val serialVersionUID = 1L
    }

    private var image: Image? = null
    private var imageWidth: Int = 1
    private var imageHeight: Int = 1
    private var scaled: Image? = null

    fun setImage(img: BufferedImage) {
        this.image = img
        this.imageHeight = img.height
        this.imageWidth = img.width
        this.invalidate()
        revalidate()
        repaint()
    }

    fun clearImage() {
        this.image = null
        this.scaled = null
        this.imageHeight = 1
        this.imageWidth = 1
        this.invalidate()
        revalidate()
        repaint()
    }

    override fun invalidate() {
        super.invalidate()

        if (image == null) return

        val width = max(50, width)
        val height = max(50, height)

        // With larger ratios, the quality of the png will be too low for convenient viewing
        val ratio = min(width.toDouble() / imageWidth, height.toDouble() / imageHeight).coerceAtMost(2.0)

        val scaledImageWidth = (imageWidth * ratio).toInt()
        val scaledImageHeight = (imageHeight * ratio).toInt()

        scaled = image?.getScaledInstance(scaledImageWidth, scaledImageHeight, Image.SCALE_SMOOTH)
    }

    override fun getPreferredSize(): Dimension {
        return if (image == null) {
            Dimension(200, 200)
        }
        else Dimension(image!!.getWidth(this), image!!.getHeight(this))
    }

    public override fun paintComponent(g: Graphics) {
        super.paintComponent(g)
        if (this.scaled != null) {
            g.drawImage(scaled, 0, 0, null)
        }
    }
}

package nl.hannahsten.texifyidea.ui

import com.intellij.util.ui.JBUI
import java.awt.Component
import javax.swing.DefaultListCellRenderer
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.ListCellRenderer

/**
 * Custom cell renderer to be used for popup choosers.
 * Adds some indentation to the list cell.
 *
 * @author Abby Berkers
 */
open class PopupChooserCellRenderer : ListCellRenderer<String> {

    override fun getListCellRendererComponent(list: JList<out String>?, value: String?, position: Int, isSelected: Boolean, hasFocus: Boolean): Component {
        val renderer = DefaultListCellRenderer().getListCellRendererComponent(
            list, value, position,
            isSelected, hasFocus
        ) as JLabel
        renderer.border = JBUI.Borders.empty(1, 8, 1, 1)
        return renderer
    }
}

package nl.hannahsten.texifyidea.ui

import com.intellij.openapi.ui.DialogBuilder
import java.awt.BorderLayout
import javax.swing.JLabel
import javax.swing.JPanel

/**
 * A dialog that contains a brief explanation about configuring inverse search with Skim.
 *
 * @author Stephan Sundermann
 */
class SkimConfigureInverseSearchDialog {

    init {
        DialogBuilder().apply {
            setTitle("Configure Inverse Search")

            val body = JLabel("In the Skim settings, go to the Sync tab. Select Custom as Preset, and as a command provide 'idea' and set arguments to '--line %line %file'

See the wiki for more information.")
            // Create panel.
            val panel = JPanel()
            panel.layout = BorderLayout()
            panel.add(body, BorderLayout.CENTER)
            setCenterPanel(panel)

            removeAllActions()
            addOkAction()
            show()
        }
    }
}

package nl.hannahsten.texifyidea.ui

import com.intellij.ide.plugins.PluginManager
import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.projectView.ProjectViewNode
import com.intellij.ide.projectView.ProjectViewNodeDecorator
import nl.hannahsten.texifyidea.util.magic.IconMagic
import java.util.*

/**
 * @author Hannah Schellekens
 */
class TeXiFyProjectViewNodeDecorator : ProjectViewNodeDecorator {

    private fun setIcon(projectViewNode: ProjectViewNode<*>, presentationData: PresentationData) {
        val file = projectViewNode.virtualFile ?: return
        if (file.isDirectory) {
            return
        }

        var extension = file.extension ?: return
        if (extension == "gz") {
            extension = "synctex.gz"

            if (!file.name.lowercase(Locale.getDefault()).endsWith("synctex.gz")) {
                return
            }
        }

        // Allow Material design plugins to take over the icons
        // For file types registered in plugin.xml this happens automatically
        if (PluginManager.getLoadedPlugins().none { it.name.contains("Material") }) {
            val icon = IconMagic.fileIcons[extension.lowercase(Locale.getDefault())] ?: return
            presentationData.setIcon(icon)
        }
    }

    override fun decorate(projectViewNode: ProjectViewNode<*>, presentationData: PresentationData) {
        setIcon(projectViewNode, presentationData)
    }
}

package nl.hannahsten.texifyidea.util

import nl.hannahsten.texifyidea.lang.commands.Argument
import nl.hannahsten.texifyidea.lang.commands.OptionalArgument

/**
 * Compute the optional power set of a set of arguments, preserving the ordering.
 *
 * Best explained with an example. Given a set ```[a]{b}{c}[d]```, it returns the sets
 *
 *  - ```{b}{c}```
 *  - ```[a]{b}{c}```
 *  - ```{b}{c}[d]```
 *  - ```[a]{b}{c}[d]```
 *
 */
fun Set<Argument>.optionalPowerSet(): Set<Set<Argument>> = setOf(this) +
    mapNotNull {
        if (it is OptionalArgument) filter { e -> e != it }.toSet().optionalPowerSet()
        else null
    }.flatten()
        .distinctBy { it.joinToString("") }
        .toSet()

package nl.hannahsten.texifyidea.util

import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import java.util.*

/**
 * Get the lowercase token type (including `@`) of the BibTeX entry (e.g. `@article`).
 */
fun BibtexEntry.tokenType(): String = type.text.lowercase(Locale.getDefault())

/**
 * Get the token type (excluding `@`) of the BibTeX entry (e.g. `article`).
 */
fun BibtexEntry.tokenName(): String = tokenType().substring(1)

/**
 * Get all the tags in the entry.
 */
fun BibtexEntry.tags(): Collection<BibtexTag> = childrenOfType(BibtexTag::class)

/**
 * Get all the key objects in the entry.
 */
fun BibtexEntry.keys(): Collection<BibtexKey> = childrenOfType(BibtexKey::class)

/**
 * Get the first key in the entry, or `null` when there are no keys in the entry.
 */
fun BibtexEntry.firstKey() = keys().firstOrNull()

/**
 * Get all the names of all entry's keys.
 */
fun BibtexEntry.keyNames(): Collection<String> = keys().map { it.text }

/**
 * Checks if the entry is a @string.
 */
fun BibtexEntry.isString() = tokenName().lowercase(Locale.getDefault()) == "string"

/**
 * Checks if the entry is a @preamble.
 */
fun BibtexEntry.isPreamble() = tokenName().lowercase(Locale.getDefault()) == "preamble"

/**
 * Get the key of the BibTeX tag.
 */
fun BibtexTag.key(): BibtexKey? = firstChildOfType(BibtexKey::class)

/**
 * Get the key of the BibTeX tag in string form.
 */
fun BibtexTag.keyName(): String? = key()?.text

/**
 * Get the content of the BibTeX tag.
 */
fun BibtexTag.content(): BibtexContent? = firstChildOfType(BibtexContent::class)

/**
 * Get the name/identifier of the bibtex id.
 */
fun BibtexId.idName(): String = text.substringEnd(1)

/**
 * Evaluates the string contents and returns a nicely concatenated version.
 *
 * E.g. `{Bambi} # space # "Broodje"` with `@string{space=" "}` will become `Bambi Broodje`.
 */
fun BibtexContent.evaluate(): String {
    val result = StringBuilder()

    for (string in childrenOfType(BibtexString::class)) {
        val braced = string.bracedString
        val quoted = string.quotedString
        val defined = string.definedString

        if (braced != null) {
            result.append(braced.evaluate())
        }
        else if (quoted != null) {
            result.append(quoted.evaluate())
        }
        else if (defined != null) {
            result.append(defined.evaluate())
        }
    }

    if (result.isEmpty()) {
        result.append(text)
    }

    return result.toString()
}

/**
 * Returns the value of the defined string.
 *
 * When no value could be found, `$IDENTIFIER` is returned where `IDENTIFIER` is the name of the (un)defined string.
 *
 * E.g. `@string{ test = "Hello"}` and defined string `test` becomes `Hello`.
 */
fun BibtexDefinedString.evaluate(): String {
    val file = containingFile
    val stringName = text

    if (file == null) {
        return ""
    }

    // Look up all string entries.
    for (entry in file.childrenOfType(BibtexEntry::class)) {
        val token = entry.tokenName().lowercase(Locale.getDefault())
        if (token != "string") {
            continue
        }

        val tags = entry.tags()
        if (tags.isEmpty()) {
            continue
        }

        val tag = tags.first()
        val key = tag.keyName() ?: continue
        if (key != stringName) {
            continue
        }

        val content = tag.content ?: continue

        // Prevent perpetual recursion.
        if (key == content.text) {
            continue
        }

        return content.evaluate()
    }

    return "${'$'}$stringName${'$'}"
}

/**
 * Returns the contents of the braced string.
 *
 * E.g. `{{T}est $\alpha$ {H}ello}` becomes `{T}est $\alpha$ {H}ello`.
 */
fun BibtexBracedString.evaluate(): String {
    val text = text
    return text.substring(1 until text.length - 1)
}

/**
 * Returns the contents of the quoted string.
 *
 * E.g. `"Test Hello"` becomes `Test Hello`.
 */
fun BibtexQuotedString.evaluate(): String {
    val text = text
    return text.substring(1 until text.length - 1)
}

package nl.hannahsten.texifyidea.util

/**
 * @author Hannah Schellekens
 */
object Clipboard {

    /**
     * Takes the complete clipboard contents (must have an html data flavor) and extracts the html (thus dropping
     * the header).
     *
     * @return null when it could not find html
     */
    @JvmStatic
    fun extractHtmlFromClipboard(clipboardContents: String): String? {
        return clipboardContents.indexOf("<html", ignoreCase = true)
            .takeIf { it >= 0 }
            ?.let { clipboardContents.substring(it) }
    }

    /**
     * Extracts the fragment html from the given contents.
     *
     * @return null when it could not find the fragment.
     */
    @JvmStatic
    fun extractHtmlFragmentFromClipboard(clipboardContents: String) = clipboardContents
        .split("<!--StartFragment-->")
        .getOrNull(1)
        ?.split("<!--EndFragment-->")
        ?.first()
}

package nl.hannahsten.texifyidea.util

import java.util.*
import java.util.stream.Collectors
import java.util.stream.Stream

/**
 * Puts all the elements of an array into a mutable map.
 *
 * The format is `key0, value0, key1, value1, ...`. This means that there must always be an even amount of elements.
 *
 * @return A map mapping `keyN` to `valueN` (see description above). When there are no elements, an empty map will be
 *          returned
 * @throws IllegalArgumentException When there is an odd amount of elements in the array.
 */
@Throws(IllegalArgumentException::class)
fun <T> mutableMapOfArray(args: Array<out T>): MutableMap<T, T> {
    if (args.isEmpty()) {
        return HashMap()
    }

    if (args.size % 2 != 0) {
        throw IllegalArgumentException("Must have an even number of elements, got ${args.size} instead.")
    }

    val map: MutableMap<T, T> = HashMap()
    for (i in 0 until args.size - 1 step 2) {
        map[args[i]] = args[i + 1]
    }

    return map
}

/**
 * Puts some elements into a mutable map.
 *
 * The format is `key0, value0, key1, value1, ...`. This means that there must always be an even amount of elements.
 *
 * @return A map mapping `keyN` to `valueN` (see description above). When there are no elements, an empty map will be
 *          returned
 * @throws IllegalArgumentException When there is an odd amount of elements in the array.
 */
fun <T> mutableMapOfVarargs(vararg args: T): MutableMap<T, T> = mutableMapOfArray(args)

/**
 * Puts some into a mutable map.
 *
 * The format is `key0, value0, key1, value1, ...`. This means that there must always be an even amount of elements.
 *
 * @return A map mapping `keyN` to `valueN` (see description above). When there are no elements, an empty map will be
 *          returned
 * @throws IllegalArgumentException When there is an odd amount of elements in the array.
 */
fun <T> mapOfVarargs(vararg args: T): Map<T, T> = mutableMapOfArray(args)

/**
 * Gets a random element from the list using the given random object.
 */
fun <T> List<T>.randomElement(random: Random): T = this[random.nextInt(this.size)]

/**
 * Looks up keys in the map that has the given `value`.
 *
 * @return All keys with the given value.
 */
fun <K, V> Map<K, V>.findKeys(value: V): Set<K> {
    return entries.asSequence()
        .filter { (_, v) -> v == value }
        .map { it.key }
        .toSet()
}

/**
 * Finds at least `amount` elements matching the given predicate.
 *
 * @param amount
 *          How many items the collection must contain at least in order to return true. Must be nonnegative.
 * @return `true` when `amount` or more elements in the collection match the given predicate.
 */
inline fun <T> Collection<T>.findAtLeast(amount: Int, predicate: (T) -> Boolean): Boolean {
    require(amount >= 0) { "Amount must be positive." }

    // Edge cases.
    when (amount) {
        0 -> none(predicate)
        1 -> any(predicate)
    }

    // More than 1 item, iterate.
    var matches = 0
    for (element in this) {
        if (predicate(element)) {
            matches += 1
            if (matches >= amount) {
                return true
            }
        }
    }

    return false
}

/**
 * Checks if all given predicates can be matched at least once.
 *
 * @return `true` if all predicates match for at least 1 element in the collection, `false` otherwise.
 */
inline fun <T> Collection<T>.anyMatchAll(predicate: (T) -> Boolean, vararg predicates: (T) -> Boolean): Boolean {
    val matches = BooleanArray(predicates.size + 1)
    var matchCount = 0
    for (element in this) {
        for (i in predicates.indices) {
            if (!matches[i] && predicates[i](element)) {
                matches[i] = true
                matchCount += 1
            }
        }

        if (!matches.last() && predicate(element)) {
            matches[matches.size - 1] = true
            matchCount += 1
        }
    }
    return matchCount == matches.size
}

/**
 * Checks if the map contains the given value as either a key or value.
 */
fun <T> Map<T, T>.containsKeyOrValue(value: T) = containsKey(value) || containsValue(value)

/**
 * Collects stream to [List].
 */
fun <T> Stream<T>.list(): List<T> = this.mutableList()

/**
 * Collects stream to [MutableList].
 */
fun <T> Stream<T>.mutableList(): MutableList<T> = this.collect(Collectors.toList())

/**
 * Collects stream to [Set].
 */
fun <T> Stream<T>.set(): Set<T> = this.mutableSet()

/**
 * Collects stream to [MutableSet]
 */
fun <T> Stream<T>.mutableSet(): MutableSet<T> = this.collect(Collectors.toSet())

/**
 * Converts the collection to a vector.
 */
fun <T> Collection<T>.toVector() = Vector(this)

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand
import nl.hannahsten.texifyidea.lang.commands.RequiredFileArgument
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexParameter
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.*
import java.util.stream.Collectors

/**
 * Finds all defined commands within the project.
 *
 * @return The found commands.
 */
fun Project.findCommandDefinitions(): Collection<LatexCommands> {
    return LatexDefinitionIndex.getItems(this).filter {
        it.isCommandDefinition()
    }
}

/**
 * Get all commands that include other files, including backslashes.
 */
fun getIncludeCommands(): Set<String> {
    return LatexRegularCommand.values()
        .filter { command -> command.arguments.any { it is RequiredFileArgument } }
        .map { "\\" + it.command }
        .toSet()
}

/**
 * Expand custom commands in a given text once, using its definition in the [LatexCommandsIndex].
 */
fun expandCommandsOnce(inputText: String, project: Project, file: PsiFile?): String? {
    var text = inputText
    // Get all the commands that are used in the input text.
    val commandsInText = LatexPsiHelper(project).createFromText(inputText).childrenOfType(LatexCommands::class)

    for (command in commandsInText) {
        // Expand the command once, and replace the command with the expanded text
        val commandExpansion = LatexCommandsIndex.getCommandsByNames(file ?: return null, *CommandMagic.commandDefinitionsAndRedefinitions.toTypedArray())
            .firstOrNull { it.getRequiredArgumentValueByName("cmd") == command.text }
            ?.getRequiredArgumentValueByName("def")
        text = text.replace(command.text, commandExpansion ?: command.text)
    }
    return text
}

/**
 * Get the index of the parameter in the command. This includes both required and optional parameters.
 */
fun LatexParameter.indexOf() = indexOfChildByType<LatexParameter, LatexCommands>()

/**
 * Get the predefined [LatexCommand] if a matching one could be found.
 * When multiple versions are available, only a random one will be selected.
 */
fun LatexCommands.defaultCommand(): LatexCommand? {
    return LatexCommand.lookup(this.name)?.firstOrNull()
}

fun LatexCommands.isFigureLabel(): Boolean =
    name in project.getLabelDefinitionCommands() && inDirectEnvironment(EnvironmentMagic.figures)

fun getCommandsInFiles(files: MutableSet<PsiFile>, originalFile: PsiFile): Collection<LatexCommands> {
    val project = originalFile.project
    val searchFiles = files.stream()
        .map { obj: PsiFile -> obj.virtualFile }
        .collect(Collectors.toSet())
    searchFiles.add(originalFile.virtualFile)
    val scope = GlobalSearchScope.filesScope(project, searchFiles)
    return LatexCommandsIndex.getItems(project, scope)
}

package nl.hannahsten.texifyidea.util

/**
 * @author Hannah Schellekens
 */
open class Container<T> @JvmOverloads constructor(
    /**
     * The item that is contained in the container (Duh).
     */
    var item: T? = null
) {

    override fun toString() = "Container[$item]"

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Container<*>) return false
        return item == other.item
    }

    override fun hashCode() = item?.hashCode() ?: 0
}

package nl.hannahsten.texifyidea.util

import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.generateServiceName
import nl.hannahsten.texifyidea.remotelibraries.mendeley.MendeleyLibrary
import nl.hannahsten.texifyidea.remotelibraries.zotero.ZoteroLibrary

/**
 * This class collects attributes for any sensitive data stored by TeXiFy.
 */
object CredentialAttributes {

    object Zotero {

        val userAttributes = createCredentialsAttributes(ZoteroLibrary.NAME)
    }

    object Mendeley {

        val tokenAttributes = createCredentialsAttributes("${MendeleyLibrary.NAME}-token")

        val refreshTokenAttributes = createCredentialsAttributes("${MendeleyLibrary.NAME}-refresh-token")
    }

    fun createCredentialsAttributes(key: String): CredentialAttributes = CredentialAttributes(generateServiceName("TeXiFy", key))
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.CaretModel
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement

/**
 * Gets all the indentation characters of the line of the given lineNumber.
 *
 * @param lineNumber
 *              The line number of the line to get the indentation of.
 * @return A string containing all the indentation characters. `empty string` when problems arise.
 */
fun Document.lineIndentation(lineNumber: Int): String {
    val result = StringBuilder()

    val lineStart = this.getLineStartOffset(lineNumber)
    val lineEnd = this.getLineEndOffset(lineNumber)
    val line = this.getText(TextRange(lineStart, lineEnd))

    for (i in line.indices) {
        if (line[i] == ' ' || line[i] == '\t') {
            result.append(line[i])
        }
        else break
    }

    return result.toString()
}

/**
 * Gets all the indentation characters of the line at the given offset.
 *
 * @param offset The offset of the line to get the indentation of.
 * @return A string containing all the indentation characters. `empty string` when problems arise.
 */
fun Document.lineIndentationByOffset(offset: Int) = lineIndentation(getLineNumber(offset))

/**
 * Places the given string into the document over the given range.
 */
fun Document.replaceString(range: TextRange, string: String) = replaceString(range.startOffset, range.endOffset, string)

/**
 * Get the text in the document at range `offset..1`.
 */
operator fun Document.get(offset: Int) = if (offset < textLength) getText(TextRange.from(offset, 1)) else ""

/**
 * Get the text in the given range.
 */
operator fun Document.get(range: IntRange) = getText(range.toTextRange())

/**
 * Replaces a single character in the document.
 *
 * @see [Document.replaceString]
 */
operator fun Document.set(offset: Int, value: CharSequence) = replaceString(offset, offset + 1, value)

/**
 * @see [Document.replaceString]
 */
operator fun Document.set(range: IntRange, value: CharSequence) = replaceString(range.first, range.last, value)

/**
 * Deletes the given element from the document.
 *
 * @param element
 *         The element to remove from the document.
 */
fun Document.deleteElement(element: PsiElement) {
    val offset = element.textOffset
    deleteString(offset, offset + element.textLength)
}

/**
 * Inserts a string into the document and moves the caret to the end of the inserted string.
 * Executes the insertion as a runWriteAction.
 *
 * @param offset
 *              Where to insert the string.
 * @param string
 *              The string to insert.
 */
fun Editor.insertAndMove(offset: Int, string: String) {
    val document = this.document
    WriteCommandAction.runWriteCommandAction(project) {
        document.insertString(offset, string)
    }
    caretModel.moveToOffset(caretModel.offset + string.length)
}

/**
 * Inserts the given string at the current caret offset, and moves to the end of the inserted string.
 *
 * @see insertAndMove
 */
fun Editor.insertAtCaretAndMove(string: String) {
    insertAndMove(caretOffset(), string)
}

/**
 * @see [CaretModel.getOffset]
 */
fun Editor.caretOffset() = caretModel.offset

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * Finds all environment definition commands within the project.
 *
 * @return The found definition commands.
 */
fun Project.findEnvironmentDefinitions(): Collection<LatexCommands> {
    return LatexDefinitionIndex.getItems(this).filter {
        it.name in CommandMagic.environmentDefinitions
    }
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import java.util.regex.Pattern

/**
 * Returns `1` when `true`, returns `0` when `false`.
 */
val Boolean.int: Int
    get() = if (this) 1 else 0

// Copied from grazie utils
fun Boolean?.orTrue() = this ?: true
fun Boolean?.orFalse() = this ?: false

/**
 * Creates a pair of two objects, analogous to [to].
 */
infix fun <T1, T2> T1.and(other: T2) = Pair(this, other)

/**
 * Executes the given run write action.
 */
fun runWriteAction(writeAction: () -> Unit) {
    ApplicationManager.getApplication().runWriteAction(writeAction)
}

fun runWriteCommandAction(project: Project, writeCommandAction: () -> Unit) {
    WriteCommandAction.runWriteCommandAction(project, writeCommandAction)
}

/**
 * Converts an [IntRange] to [TextRange].
 */
fun IntRange.toTextRange() = TextRange(this.first, this.last + 1)

/**
 * Get the length of an [IntRange].
 */
val IntRange.length: Int
    get() = endInclusive - start

/**
 * Converts the range to a range representation with the given seperator.
 * When the range has size 0, it will only print the single number.
 */
fun IntRange.toRangeString(separator: String = "-") = if (start == endInclusive) start else "$start$separator$endInclusive"

/**
 * Shift the range to the right by the number of places given.
 */
fun IntRange.shiftRight(displacement: Int): IntRange {
    return (this.first + displacement)..(this.last + displacement)
}

/**
 * Converts a [TextRange] to [IntRange].
 */
fun TextRange.toIntRange() = startOffset..endOffset

/**
 * Easy access to [java.util.regex.Matcher.matches].
 */
fun Pattern.matches(sequence: CharSequence?) = if (sequence != null) matcher(sequence).matches() else false

package nl.hannahsten.texifyidea.util

import java.util.*

/**
 * @author Hannah Schellekens
 */
object Kindness {

    private val random = Random()

    private val kindWords = listOf(
        "You are beautiful. You matter. You are awesome.",
        "While it is always best to believe in oneself, a little help from others is a great blessing.",
        "You have light and peace inside of you. If you let it out, you can change the world around you.",
        "You can do the thing!",
        "I hope that you are having a fantastic day.",
        "Confidence comes not from always being right but from not fearing to be wrong.",
        "There is nothing wrong with letting people who love you help you.",
        "If you look for the light you can often find it, but if you look for the dark that " + "is all you will ever see.",
        "Sometimes the best way to solve your own problems is to help somebody else."
    )

    @JvmStatic
    fun getKindWords(): String = kindWords.randomElement(random)
}

package nl.hannahsten.texifyidea.util

import io.ktor.client.call.*
import io.ktor.client.statement.*

/**
 * Extracts urls from a Link header that follows [rfc5988](https://www.rfc-editor.org/rfc/pdfrfc/rfc5988.txt.pdf).
 */
fun String.parseLinkHeader(): Map<String, String> {
    return Regex("<(?<url>[^>]*)>; rel=\"(?<key>\\w+)\"").findAll(this)
        .filter { it.groups["key"] != null && it.groups["url"] != null }
        .map { it.groups["key"]!!.value to it.groups["url"]!!.value }
        .toMap()
}

/**
 * Use the Link header to handle a paginated response, building any subsequent request from [nextPageRequest].
 *
 * @return a Pair with the [HttpResponse] of the last executed request and the complete body.
 *  The last response can be used to handle failing requests appropriately.
 */
suspend fun HttpResponse.paginateViaLinkHeader(nextPageRequest: suspend (String) -> HttpResponse): Pair<HttpResponse, String> {
    val resultString = StringBuilder().append(body<String>())

    var lastResponse = this
    while (lastResponse.hasNextPage()) {
        lastResponse = lastResponse.getNextPage(nextPageRequest) ?: return Pair(lastResponse, resultString.toString())
        resultString.append(lastResponse.body<String>())
    }

    return Pair(lastResponse, resultString.toString())
}

/**
 * Get the next page from the next url in the Link header.
 */
suspend fun HttpResponse.getNextPage(nextPageRequest: suspend (String) -> HttpResponse): HttpResponse? {
    val nextUrl = headers["Link"]?.parseLinkHeader()?.get("next") ?: return null

    return nextPageRequest(nextUrl)
}

/**
 * Check if an [HttpResponse] has a Link header with a next url.
 *
 * If it does not, the current request is not paginated or the current pagination request does not have a next page.
 */
fun HttpResponse.hasNextPage(): Boolean = headers["Link"]?.let { it.parseLinkHeader()["next"] != null } ?: false

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import java.io.File
import kotlin.io.path.Path

/**
 * Try to find a latexmkrc file (see the latexmk man page).
 */
object LatexmkRcFileFinder {

    // Note: this cannot be a lazy val because then if there is any exception, the stacktrace will not be shown but be hidden by a NoClassDefFoundError. This way, we will have an ExceptionInInitializerError with will have the original stacktrace as well.
    private var systemLatexmkRcFile: VirtualFile? = null
        get() {
            val paths = mutableListOf<String?>()
            // 1
            if (SystemInfo.isLinux) {
                paths += listOf(
                    "/opt/local/share/latexmk/",
                    "/usr/local/share/latexmk/",
                    "/usr/local/lib/latexmk/",
                    "/cygdrive/local/share/latexmk/"
                ).flatMap { listOf(it + "latexMk", it + "latexmkrc") }
            }
            else if (SystemInfo.isWindows) {
                paths += listOf(
                    "C:\\latexmk\\LatexMk",
                    "C:\\latexmk\\latexmkrc"
                )
            }

            System.getenv("LATEXMKRCSYS")?.let { paths.add(it) }

            // 2
            paths += listOf(
                System.getenv("XDG_CONFIG_HOME")?.let { Path(it, "latexmk", "latexmkrc").toString() },
                System.getenv("HOME")?.let { Path(it, ".latexmkrc").toString() },
                System.getenv("USERPROFILE")?.let { Path(it, ".latexmkrc").toString() },
                System.getenv("HOME")?.let { Path(it, ".config", ".latexmkrc").toString() },
            )

            try {
                paths.filterNotNull()
                    .forEach {
                        LocalFileSystem.getInstance().findFileByIoFile(File(it))?.let { file ->
                            field = file
                            return file
                        }
                    }
            }
            // No idea why
            catch (e: ProcessCanceledException) {
                return null
            }

            return null
        }

    private val isSystemLatexmkRcFilePresent: Boolean = systemLatexmkRcFile != null

    private fun isLocalLatexmkRcFilePresent(compilerArguments: String?, workingDir: String?) = getLocalLatexmkRcFile(compilerArguments, workingDir) != null

    /**
     * Get the first latexmkrc file we can find locally (in the project).
     */
    private fun getLocalLatexmkRcFile(compilerArguments: String?, workingDir: String?): VirtualFile? {
        // 3
        if (workingDir != null) {
            listOf(
                workingDir + File.separator + "latexmkrc",
                workingDir + File.separator + ".latexmkrc"
            ).forEach {
                LocalFileSystem.getInstance().findFileByPath(it)?.let { file -> return file }
            }
        }

        // 4
        if (compilerArguments != null) {
            val arguments = compilerArguments.splitWhitespace().dropWhile { it.isBlank() }
            if (arguments.contains("-r") && arguments.last() != "-r") {
                val path = arguments[arguments.indexOf("-r") + 1]
                LocalFileSystem.getInstance().findFileByPath(path)?.let { file -> return file }
            }
        }

        return null
    }

    fun isLatexmkRcFilePresent(runConfig: LatexRunConfiguration): Boolean {
        val isPresent = isSystemLatexmkRcFilePresent || isLocalLatexmkRcFilePresent(runConfig.compilerArguments, runConfig.mainFile?.parent?.path)

        // The first time, by default don't override what's in the latexmkrc (but avoid resetting the user chosen output format)
        if (isPresent && !runConfig.hasBeenRun) {
            runConfig.outputFormat = LatexCompiler.Format.DEFAULT
        }
        return isPresent
    }

    /**
     * Get TEXINPUTS from latexmkrc.
     */
    private fun getTexinputs(file: VirtualFile): String? {
        return """ensure_path\('TEXINPUTS',\s*'(?<path>[^']+)'\)""".toRegex().find(file.inputStream.reader().readText())?.groups?.get("path")?.value
    }

    private var usesLatexmkrc: Boolean? = null

    /**
     * Get the first TEXINPUTS we can find in latexmkrc files.
     * Cached because searching involves quite some system calls, and it's a rarely used feature.
     */
    fun getTexinputsVariable(someFile: PsiFile?, runConfig: LatexRunConfiguration?): String? {
        return if (usesLatexmkrc == false) {
            null
        }
        else {
            val texinputs = getTexinputsVariableNoCache(someFile, runConfig)
            if (usesLatexmkrc == null) {
                usesLatexmkrc = texinputs != null
            }
            texinputs
        }
    }

    /**
     * Check the (first) latexmkrc file for any additions to TEXINPUTS and return that if present.
     *
     * @param someFile Any file, the parent directories will be searched.
     * @param runConfig Run configuration to check for working directory and arguments.
     */
    private fun getTexinputsVariableNoCache(someFile: PsiFile?, runConfig: LatexRunConfiguration?): String? {
        systemLatexmkRcFile?.let { return getTexinputs(it) }
        if (runConfig != null) {
            getLocalLatexmkRcFile(runConfig.compilerArguments, runConfig.mainFile?.parent?.path)?.let { return getTexinputs(it) }
        }
        // File could be anywhere if run configurations are not used, but searching the whole project could be too expensive
        someFile?.virtualFile?.parent?.let { parentDir ->
            parentDir.findChild(".latexmkrc")?.let { return getTexinputs(it) }
            parentDir.findChild("latexmkrc")?.let { return getTexinputs(it) }
        }
        val project = someFile?.project ?: return null
        val projectDir = project.guessProjectDir()
        if (projectDir?.isValid == false) return null
        projectDir?.findChild(".latexmkrc")?.let { return getTexinputs(it) }
        projectDir?.findChild("latexmkrc")?.let { return getTexinputs(it) }
        projectDir?.children?.forEach { directory ->
            directory?.findChild(".latexmkrc")?.let { return getTexinputs(it) }
            directory?.findChild("latexmkrc")?.let { return getTexinputs(it) }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.diagnostic.Logger

/**
 * Log messages to idea.log. During plugin development, it logs to build/idea-sandbox/system/log/idea.log.
 * You can add a tab to the run configuration tool window by adding 'Specify logs to be shown in console' to the runIde run configuration.
 * To view debug logs, in the dev instance go to Help > Diagnostic Tools > Debug Log Settings and add #nl.hannahsten.texifyidea.util.Log.
 * Then in the console, make sure the filter is set to All instead of Warning.
 *
 * @author Hannah Schellekens
 */
object Log {

    private val logger: Logger by lazy { Logger.getInstance(Log::class.java) }

    private const val prefix = "TEXIFY-IDEA -"

    /**
     * Sends an info message to the IntelliJ logger.
     *
     * All messages start with the prefix `TEXIFY-IDEA - `.
     */
    fun info(message: String) = logger.info("$prefix $message")

    fun warn(message: String) = logger.warn("$prefix $message")

    fun debug(message: String) = logger.debug("$prefix $message")

    fun error(message: String) = logger.error("$prefix $message")
}

package nl.hannahsten.texifyidea.util

import java.util.*

/**
 * @author Hannah Schellekens
 */
private val ROMAN = TreeMap<Int, String>().apply {
    this[1000] = "M"
    this[500] = "D"
    this[100] = "C"
    this[50] = "L"
    this[10] = "X"
    this[5] = "V"
    this[1] = "I"
}

/**
 * Turns a given integer into a roman numeral.
 *
 * @receiver The (positive) integer to convert to roman.
 * @return The roman representation of said integer.
 * @throws IllegalArgumentException
 *          When the integer is smaller or equal to 0.
 */
fun Int.toRoman(): String {
    check(this > 0) { "Integer must be positive, got $this" }

    val fromMap = ROMAN.floorKey(this)
    return if (this == fromMap) {
        ROMAN[this]!!
    }
    else ROMAN[fromMap] + (this - fromMap!!).toRoman()
}

/**
 * @see Integer.toHexString
 */
fun Int.toHexString(): String = Integer.toHexString(this)

/**
 * Return true if [this] has a non-empty overlap with [other].
 */
fun IntRange.overlaps(other: IntRange): Boolean {
    return this.last >= other.first && other.last >= this.first
}

/**
 * Merge [this] with all [others] to result in one IntRange, assuming that [this] overlaps with all [others].
 */
fun IntRange.merge(others: Iterable<IntRange>): IntRange {
    val all = mutableSetOf(this).also { it.addAll(others) }
    return IntRange(all.minByOrNull { it.first }!!.first, all.maxByOrNull { it.last }!!.last)
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.file.LatexExternalPackageInclusionCache
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.files.*
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.PackageMagic
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.toStringMap

/**
 * @author Hannah Schellekens
 */
object PackageUtils {

    /**
     * List containing all the packages that are available on CTAN.
     *
     * This is a static list for now, will be made dynamic (index CTAN programmatically) in the future.
     */
    val CTAN_PACKAGE_NAMES: List<String> = javaClass
        .getResourceAsStream("/nl/hannahsten/texifyidea/packages/package.list")
        ?.bufferedReader()
        ?.readLine()
        ?.split(";")
        ?.toList() ?: emptyList()

    /**
     * Inserts a usepackage statement for the given package in a certain file.
     *
     * @param file
     *          The file to add the usepackage statement to.
     * @param packageName
     *          The name of the package to insert.
     * @param parameters
     *          Parameters to add to the statement, `null` or empty string for no parameters.
     */
    @JvmStatic
    fun insertUsepackage(file: PsiFile, packageName: String, parameters: String?) {
        if (!file.isWritable) return

        if (!TexifySettings.getInstance().automaticDependencyCheck) {
            return
        }

        val commands = file.commandsInFile()

        val commandName = if (file.isStyleFile() || file.isClassFile()) "\\RequirePackage" else "\\usepackage"

        var last: LatexCommands? = null
        for (cmd in commands) {
            if (commandName == cmd.commandToken.text) {
                // Do not insert below the subfiles package, it should stay last
                if (cmd.getRequiredParameters().contains("subfiles")) {
                    break
                }
                else {
                    last = cmd
                }
            }
        }

        val prependNewLine: Boolean
        // The anchor after which the new element will be inserted
        val anchorAfter: PsiElement?

        // When there are no usepackage commands: insert below documentclass.
        if (last == null) {
            val classHuh = commands.asSequence()
                .filter { cmd ->
                    "\\documentclass" == cmd.commandToken
                        .text || "\\LoadClass" == cmd.commandToken.text
                }
                .firstOrNull()
            if (classHuh != null) {
                anchorAfter = classHuh
                prependNewLine = true
            }
            else {
                // No other sensible location can be found
                anchorAfter = null
                prependNewLine = false
            }
        }
        // Otherwise, insert below the lowest usepackage.
        else {
            anchorAfter = last
            prependNewLine = true
        }

        var command = commandName
        command += if (parameters == null || "" == parameters) "" else "[$parameters]"
        command += "{$packageName}"

        val newNode = LatexPsiHelper(file.project).createFromText(command).firstChild.node

        // Don't run in a write action, as that will produce a SideEffectsNotAllowedException for INVOKE_LATER

        // Avoid "Attempt to modify PSI for non-committed Document"
        // https://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/modifying_psi.html?search=refac#combining-psi-and-document-modifications
        PsiDocumentManager.getInstance(file.project)
            .doPostponedOperationsAndUnblockDocument(file.document() ?: return)
        PsiDocumentManager.getInstance(file.project).commitDocument(file.document() ?: return)
        runWriteAction {
            // Avoid NPE, see #3083 (cause unknown)
            if (anchorAfter != null && com.intellij.psi.impl.source.tree.TreeUtil.getFileElement(anchorAfter.parent.node) != null) {
                val anchorBefore = anchorAfter.node.treeNext
                @Suppress("KotlinConstantConditions")
                if (prependNewLine) {
                    val newLine = LatexPsiHelper(file.project).createFromText("\n").firstChild.node
                    anchorAfter.parent.node.addChild(newLine, anchorBefore)
                }
                anchorAfter.parent.node.addChild(newNode, anchorBefore)
            }
            else {
                // Insert at beginning
                file.node.addChild(newNode, file.firstChild.node)
            }
        }
    }

    /**
     * Inserts a usepackage statement for the given package in the root file of the fileset containing the given file.
     * Will not insert a new statement when the package has already been included, or when a conflicting package is already included.
     *
     * @param file The file to add the usepackage statement to.
     * @param pack The package to include.
     *
     * @return false if the package was not inserted, because a conflicting package is already present.
     */
    fun insertUsepackage(file: PsiFile, pack: LatexPackage): Boolean {
        if (pack.isDefault) {
            return true
        }

        if (file.includedPackages().contains(pack)) {
            return true
        }

        // Don't insert when a conflicting package is already present
        if (PackageMagic.conflictingPackages.any { it.contains(pack) }) {
            for (conflicts in PackageMagic.conflictingPackages) {
                // Assuming the package is not already included
                if (conflicts.contains(pack) && file.includedPackages().toSet()
                    .intersect(conflicts)
                    .isNotEmpty()
                ) {
                    return false
                }
            }
        }

        // Packages should always be included in the root file
        val rootFile = file.findRootFile()

        val params = pack.parameters
        val parameterString = StringUtil.join(params, ",")
        insertUsepackage(rootFile, pack.name, parameterString)

        return true
    }

    /**
     * Analyses the given file to finds all the imported tikz libraries in the included file set.
     *
     * @return A set containing all used package names.
     */
    @JvmStatic
    fun getIncludedTikzLibraries(baseFile: PsiFile): Set<String> {
        val commands = baseFile.commandsInFileSet()
        return getIncludedTikzLibraries(commands, HashSet())
    }

    /**
     * Analyses the given file to finds all the imported pgfplots libraries in the included file set.
     *
     * @return A set containing all used package names.
     */
    @JvmStatic
    fun getIncludedPgfLibraries(baseFile: PsiFile): Set<String> {
        val commands = baseFile.commandsInFileSet()
        return getIncludedPgfLibraries(commands, HashSet())
    }

    /**
     * Gets all packages imported with tikz library import commands.
     */
    @JvmStatic
    fun <T : MutableCollection<String>> getIncludedTikzLibraries(
        commands: Collection<LatexCommands>,
        result: T
    ) = getPackagesFromCommands(commands, CommandMagic.tikzLibraryInclusionCommands, result)

    /**
     * Gets all packages imported with pgf library import commands.
     */
    @JvmStatic
    fun <T : MutableCollection<String>> getIncludedPgfLibraries(
        commands: Collection<LatexCommands>,
        result: T
    ) = getPackagesFromCommands(commands, CommandMagic.pgfplotsLibraryInclusionCommands, result)

    /**
     * Analyses all the given commands and reduces it to a set of all included packages, libraries or whatever is imported
     * with the given [packageCommands].
     * Classes will be included.
     *
     * Note that not all elements returned may be valid package names.
     */
    fun <T : MutableCollection<String>> getPackagesFromCommands(
        commands: Collection<LatexCommands>,
        packageCommands: Set<String>,
        initial: T
    ): T {
        for (cmd in commands) {
            if (cmd.name !in packageCommands) {
                continue
            }

            // Just skip conditionally included packages, because it is too expensive to determine whether
            // they are really included or not
            if (cmd.parent?.firstParentOfType(LatexCommands::class)?.name == "\\" + LatexGenericRegularCommand.ONLYIFSTANDALONE.command) {
                continue
            }

            // Assume packages can be included in both optional and required parameters
            // Technically a class is not a package, but LatexCommand doesn't separate those things yet so we ignore that here as well
            val packages = setOf(
                cmd.getRequiredParameters(),
                cmd.getOptionalParameterMap().toStringMap().keys.toList()
            )

            for (list in packages) {
                if (list.isEmpty()) {
                    continue
                }

                val packageName = list[0]

                // Multiple includes.
                if (packageName.contains(",")) {
                    initial.addAll(
                        packageName.split(",").dropLastWhile(String::isNullOrEmpty)
                    )
                }
                // Single include.
                else {
                    initial.add(packageName)
                }
            }
        }

        return initial
    }
}

/**
 * @see PackageUtils.insertUsepackage
 */
fun PsiFile.insertUsepackage(pack: LatexPackage) = PackageUtils.insertUsepackage(this, pack)

/**
 * Find all included LaTeX packages in the file set of this file.
 *
 * These may be packages that are in the project, installed in the LateX distribution or somewhere else.
 * This includes packages that are included indirectly (via other packages).
 *
 * @param onlyDirectInclusions If true, only packages included directly are returned.
 * @return List of all included packages. Those who are directly included, may contain duplicates.
 */
fun PsiFile.includedPackages(onlyDirectInclusions: Boolean = false): List<LatexPackage> {
    val commands = this.commandsInFileSet()
    return includedPackages(commands, project, onlyDirectInclusions)
}

/**
 * See [includedPackages].
 */
fun includedPackages(commands: Collection<LatexCommands>, project: Project, onlyDirectInclusions: Boolean = false): List<LatexPackage> {
    val directIncludes = PackageUtils.getPackagesFromCommands(commands, CommandMagic.packageInclusionCommands, mutableListOf())
        .map { LatexPackage(it) }
    return if (onlyDirectInclusions) directIncludes else LatexExternalPackageInclusionCache.getAllIndirectlyIncludedPackages(directIncludes, project).toList()
}

package nl.hannahsten.texifyidea.util

import com.intellij.execution.RunManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.FileTypeIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.serviceContainer.AlreadyDisposedException
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.modules.LatexModuleType
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.util.files.allChildFiles
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * Get a project [GlobalSearchScope] for this project.
 */
val Project.projectSearchScope: GlobalSearchScope
    get() = GlobalSearchScope.projectScope(this)

/**
 * Get a [GlobalSearchScope] for the source folders in this project.
 */
val Project.sourceSetSearchScope: GlobalSearchScope
    get() {
        val rootManager = ProjectRootManager.getInstance(this)
        val files = rootManager.contentSourceRoots.asSequence()
            .flatMap { it.allChildFiles().asSequence() }
            .toSet()
        return GlobalSearchScope.filesWithoutLibrariesScope(this, files)
    }

/**
 * Looks for all defined document classes in the project.
 */
fun Project.findAvailableDocumentClasses(): Set<String> {
    val defines = LatexDefinitionIndex.getCommandsByName("ProvidesClass", this, sourceSetSearchScope)
    return defines.asSequence()
        .map { it.getRequiredParameters() }
        .filter { it.isNotEmpty() }
        .mapNotNull { it.firstOrNull() }
        .toSet()
}

/**
 * Get all the virtual files that are in the project of a given file type.
 */
fun Project.allFiles(type: FileType): Collection<VirtualFile> {
    if (!isInitialized) return emptyList()
    try {
        return runReadAction {
            val scope = GlobalSearchScope.projectScope(this)
            return@runReadAction FileTypeIndex.getFiles(type, scope)
        }
    }
    catch (e: IllegalStateException) {
        // Doesn't happen very often, and afaik there's no proper way of checking whether this index is initialized. See #2855
        if (e.message?.contains("Index is not created for `filetypes`") == true) {
            return emptyList()
        }
        else {
            throw e
        }
    }
}

/**
 * Get all LaTeX run configurations in the project.
 */
fun Project.getLatexRunConfigurations(): Collection<LatexRunConfiguration> {
    if (isDisposed) return emptyList()
    return RunManager.getInstance(this).allConfigurationsList.filterIsInstance<LatexRunConfiguration>()
}

/**
 * Get the run configuration that is currently selected.
 */
fun Project?.selectedRunConfig(): LatexRunConfiguration? = this?.let {
    RunManager.getInstance(it).selectedConfiguration?.configuration as? LatexRunConfiguration
}

/**
 * Get the first of the selected editors as a [TextEditor].
 * Returns `null` when there are no selected *text* editors.
 */
fun Project.currentTextEditor(): TextEditor? {
    val editors = FileEditorManager.getInstance(this).selectedEditors
    return editors.firstOrNull { it is TextEditor } as TextEditor?
}

/**
 * Checks if there is a LaTeX module in this project.
 *
 * Note: according to the documentation of ModuleType:
 *     If you need to make an action enabled in presence of a specific technology only, do this by looking for required files in the project
 *     directories, not by checking type of the current module.
 */
fun Project.hasLatexModule(): Boolean {
    if (isDisposed) return false
    return try {
        ModuleManager.getInstance(this).modules
            .any { ModuleType.get(it).id == LatexModuleType.ID }
    }
    catch (e: AlreadyDisposedException) {
        false
    }
}

/**
 * Best guess at whether this project can be considered a project containing significant LaTeX things.
 */
fun Project.isLatexProject(): Boolean {
    return hasLatexModule() ||
        getLatexRunConfigurations().isNotEmpty() ||
        (ApplicationNamesInfo.getInstance().scriptName != "idea" && allFiles(LatexFileType).isNotEmpty())
}

/**
 * True if we are probably in a unit test.
 */
fun Project.isTestProject() = name.contains("_temp_") || basePath?.contains("unitTest") == true

/**
 * Finds all section marker commands (as defined in [CommandMagic.sectionMarkers]) in the project.
 *
 * @return A list containing all the section marker [LatexCommands].
 */
fun Project.findSectionMarkers() = LatexCommandsIndex.getItems(this).filter {
    it.commandToken.text in CommandMagic.sectionMarkers
}

package nl.hannahsten.texifyidea.util

import nl.hannahsten.texifyidea.settings.TexifySettings

/**
 * Define what the replacements are for opening and closing quotes, in case that is relevant for the user setting.
 */
fun getOpenAndCloseQuotes(char: Char): Pair<String, String> {
    var openingQuotes = char.toString()
    var closingQuotes = char.toString()

    // Get the saved value to find the correct replacement
    val quoteSetting = TexifySettings.getInstance().automaticQuoteReplacement

    if (quoteSetting == TexifySettings.QuoteReplacement.LIGATURES && char == '"') {
        openingQuotes = "``"
        closingQuotes = "''"
    }
    else if (quoteSetting == TexifySettings.QuoteReplacement.COMMANDS && char == '"') {
        openingQuotes = "\\lq\\lq{}"
        closingQuotes = "\\rq\\rq{}"
    }
    else if (quoteSetting == TexifySettings.QuoteReplacement.CSQUOTES && char == '"') {
        openingQuotes = "\\enquote{"
        closingQuotes = "}"
    }
    else if (quoteSetting == TexifySettings.QuoteReplacement.LIGATURES && char == '\'') {
        openingQuotes = "`"
        closingQuotes = "'"
    }
    else if (quoteSetting == TexifySettings.QuoteReplacement.COMMANDS && char == '\'') {
        openingQuotes = "\\lq{}"
        closingQuotes = "\\rq{}"
    }
    else if (quoteSetting == TexifySettings.QuoteReplacement.CSQUOTES && char == '\'') {
        openingQuotes = "\\enquote*{"
        closingQuotes = "}"
    }

    return Pair(openingQuotes, closingQuotes)
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.util.TextRange
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import org.intellij.lang.annotations.Language
import java.io.File
import java.text.Normalizer
import java.util.*
import kotlin.math.max
import kotlin.math.min

/**
 * Capitalises the first character of the string, if present.
 */
fun String.capitalizeFirst(): String = if (this.isEmpty()) this else this[0].uppercaseChar() + substring(1, length)

/**
 * Converts the string to camel case.
 */
fun String.camelCase(): String {
    @Language("RegExp")
    val parts = lowercase(Locale.getDefault()).split(Regex("[_\\s]+"))

    val sb = StringBuilder(parts[0])
    for (i in 1 until parts.size) {
        sb.append(parts[i].capitalizeFirst())
    }

    return sb.toString()
}

/**
 * Repeats the given string a given amount of times.
 *
 * @param count
 *         The amount of times to repeat the string.
 */
fun String.repeat(count: Int) = buildString(count * this.length) {
    for (i in 0 until count) {
        append(this@repeat)
    }
}

/**
 * Takes the substring, but with inverted index, i.e. the index of the first character is `length`, the last index is `0`.
 */
fun String.substringEnd(startIndex: Int): String = substring(0, length - startIndex)

/**
 * Takes the substring, but with inverted index, i.e. the index of the first character is `length`, the last index is `0`.
 */
fun String.substringEnd(startIndex: Int, endIndex: Int): String = substring(length - endIndex, length - startIndex)

/**
 * Takes the substring, but with inverted index, i.e. the index of the first character is `length`, the last index is `0`.
 */
fun String.substringEnd(range: IntRange): String = substringEnd(range.first, range.last + 1)

/**
 * Trims `startTrim` characters from the front, and `endTrim` characters from the end.
 */
fun String.trimRange(startTrim: Int, endTrim: Int): String {
    if (startTrim + endTrim > length) return ""
    return substring(startTrim).substringEnd(endTrim)
}

/**
 * Returns the leading whitespace of a string.
 */
fun String.getIndent(): String {
    val matcher = PatternMagic.leadingWhitespace.matcher(this)
    return if (matcher.find()) matcher.group(0) else ""
}

/**
 * Appends an extension to a path only if the given path does not end in that extension.
 *
 * @param extensionWithoutDot
 *         The extension to append optionally.
 * @return A path ending with the given extension without duplications (e.g. `.tex.tex` is impossible.)
 */
fun String.appendExtension(extensionWithoutDot: String): String {
    if (extensionWithoutDot == "") return this

    val dottedExtension = ".${extensionWithoutDot.lowercase(Locale.getDefault())}"
    val thisLower = lowercase(Locale.getDefault())

    return when {
        thisLower.endsWith(dottedExtension) -> this
        endsWith('.') -> this + extensionWithoutDot
        else -> this + dottedExtension
    }
}

/**
 * Removes unnecessary indents.
 *
 * Meaning that when all lines have at least `x` characters, `x` characters will be removed.
 *
 * @return All lines with shared indents removed.
 */
fun List<String>.removeIndents(): List<String> {
    if (isEmpty()) {
        return this
    }

    val list = ArrayList<String>(size)
    val (maxIndent, _) = asSequence()
        .filter { it.isNotBlank() }
        .map { Pair(it.getIndent().length, it) }
        .minByOrNull { it.first } ?: return this

    var noContentYet = true
    for (originalLine in this) {
        if (noContentYet && originalLine.isBlank()) {
            continue
        }

        if (originalLine.isBlank()) {
            list.add("")
        }
        else {
            noContentYet = false
            list.add(originalLine.substring(maxIndent))
        }
    }

    return list
}

/**
 * Removes unnecessary indents.
 *
 * Meaning that when all lines have at least `x` characters, `x` characters will be removed.
 *
 * @return All lines with shared indents removed.
 */
fun String.removeIndents() = PatternMagic.newline.split(this)
    .toList()
    .removeIndents()
    .joinToString("\n")

/**
 * Remove all appearances of all given strings.
 */
fun String.removeAll(vararg strings: String): String {
    var formatted = this
    strings.forEach { formatted = formatted.replace(it, "") }
    return formatted
}

/**
 * Remove [string] from [this].
 */
fun String.remove(string: String): String = this.replace(string, "")

/**
 * Formats the string as a valid filename, removing not-allowed characters, in TeX-style with - as separator.
 */
fun String.formatAsFileName(): String = this.formatAsFilePath().removeAll("/", "\\")

/**
 * Formats the string as a valid filepath in our recommended LaTeX style, removing not-allowed characters, in TeX-style with - as separator. Any / or \ characters are not removed.
 */
fun String.formatAsFilePath(): String {
    val formatted = this.replace(" ", "-")
        .removeAll("<", ">", "\"", "|", "?", "*", ":") // Mostly just a problem on Windows
        .lowercase(Locale.getDefault())

    // If there are no valid characters left, use a default name.
    return formatted.ifEmpty { "myfile" }
}

/**
 * Formats the string as a valid LaTeX label name.
 */
fun String.formatAsLabel(): String {
    return this.let { Normalizer.normalize(it, Normalizer.Form.NFKD) }
        .replace(" ", "-")
        .removeAll("%", "~", "#", "\\", ",")
        .replace("[^\\x00-\\x7F]".toRegex(), "")
        .lowercase(Locale.getDefault())
}

/**
 * Split the given string on whitespace.
 */
fun String.splitWhitespace() = split(Regex("\\s+"))

/**
 * Removes HTML tags from the string.
 *
 * @return The string with HTML tags removed.
 *
 * @see [PatternMagic.htmlTag]
 */
fun String.removeHtmlTags() = this.replace(PatternMagic.htmlTag.toRegex(), "")

/**
 * Run a command in the terminal.
 * You can only use this if you are sure you don't have paths and other escaped things with spaces.
 *
 * @return The output of the command or null if an exception was thrown.
 */
fun String.runCommand(workingDirectory: File? = null) =
    runCommand(*(this.split("\\s".toRegex())).toTypedArray(), workingDirectory = workingDirectory)

fun String.runCommandWithExitCode(workingDirectory: File? = null) =
    runCommandWithExitCode(*(this.split("\\s".toRegex())).toTypedArray(), workingDirectory = workingDirectory)

/**
 * Index of first occurrence of any of the given chars. Return last index if chars do not appear in the string.
 */
fun String.firstIndexOfAny(vararg chars: Char): Int {
    var index = length - 1
    for (char in chars) {
        if (indexOf(char) != -1) index = min(index, indexOf(char))
    }
    return index
}

/** If this contains any of the given set. */
fun CharSequence.containsAny(set: Set<String>) = set.any { this.contains(it) }

/** If this starts with any of the given set. */
fun String.startsWithAny(vararg prefix: String) = prefix.any { this.startsWith(it) }

/** Shrink textrange with the given amount at both sides. */
fun TextRange.shrink(amount: Int) = TextRange(min(this.startOffset + amount, endOffset - 1), max(0, this.endOffset - amount))

/**
 * Appends a line separator.
 */
fun StringBuilder.newline() = append("\n")!!

/**
 * Encloses the string with the given prefix and suffix when the given predicate yields true.
 * Otherwise just returns this string.
 */
inline fun String.encloseWhen(prefix: String = "", suffix: String = "", predicate: () -> Boolean) = buildString {
    val predicateResult = predicate()
    if (predicateResult) {
        append(prefix)
    }
    append(this@encloseWhen)
    if (predicateResult) {
        append(suffix)
    }
}

package nl.hannahsten.texifyidea.util

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessNotCreatedException
import com.intellij.openapi.util.SystemInfo
import org.apache.maven.artifact.versioning.DefaultArtifactVersion
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit

/**
 * Information about the system other than the LatexDistribution or the OS.
 */
class SystemEnvironment {

    companion object {

        val inkscapeMajorVersion: Int by lazy {
            "inkscape --version".runCommand()
                ?.split(" ")?.getOrNull(1)
                ?.split(".")?.firstOrNull()
                ?.toInt() ?: 0
        }

        val isInkscapeInstalledAsSnap: Boolean by lazy {
            "snap list".runCommand()?.contains("inkscape") == true
        }

        /** Cache for [isAvailable]. */
        private var availabilityCache = mutableMapOf<String, Boolean>()

        /**
         * Check if [command] is available as a system command.
         */
        fun isAvailable(command: String): Boolean {
            // Not thread-safe, but don't think that's a problem here
            availabilityCache.getOrDefault(command, null)?.let { return it }

            // Has to be run with bash because command is a shell command
            val isAvailable = if (SystemInfo.isUnix) {
                val (_, exitCode) = runCommandWithExitCode("bash", "-c", "command -v $command")
                exitCode == 0
            }
            else {
                "where $command".runCommandWithExitCode().second == 0
            }
            availabilityCache[command] = isAvailable
            return isAvailable
        }

        // Assumes version will be given in the format GNOME Document Viewer 3.34.2
        val evinceVersion: DefaultArtifactVersion by lazy {
            DefaultArtifactVersion("evince --version".runCommand()?.split(" ")?.lastOrNull() ?: "")
        }
    }
}

/**
 * Run a command in the terminal.
 *
 * @return The output of the command or null if an exception was thrown.
 */
fun runCommand(vararg commands: String, workingDirectory: File? = null): String? {
    return runCommandWithExitCode(*commands, workingDirectory = workingDirectory).first
}

/**
 * See [runCommand], but also returns exit code.
 *
 * @param returnExceptionMessage Whether to return exception messages if exceptions are thrown.
 */
fun runCommandWithExitCode(vararg commands: String, workingDirectory: File? = null, timeout: Long = 3, returnExceptionMessage: Boolean = false, nonBlocking: Boolean = false): Pair<String?, Int> {
    Log.debug("Executing in ${workingDirectory ?: "current working directory"} ${GeneralCommandLine(*commands).commandLineString}")
    return try {
        val proc = GeneralCommandLine(*commands)
            .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
            .withWorkDirectory(workingDirectory)
            .createProcess()

        if (proc.waitFor(timeout, TimeUnit.SECONDS)) {
            var output = ""
            if (nonBlocking) {
                if (proc.inputStream.bufferedReader().ready()) {
                    output += proc.inputStream.bufferedReader().readText().trim()
                }
                if (proc.errorStream.bufferedReader().ready()) {
                    output += proc.errorStream.bufferedReader().readText().trim()
                }
            }
            else {
                output = proc.inputStream.bufferedReader().readText().trim() + proc.errorStream.bufferedReader().readText().trim()
            }
            Log.debug("${commands.firstOrNull()} exited with ${proc.exitValue()} ${output.take(100)}")
            return Pair(output, proc.exitValue())
        }
        else {
            var output = ""
            if (nonBlocking) {
                if (proc.inputStream.bufferedReader().ready()) {
                    output += proc.inputStream.bufferedReader().readText().trim()
                }
                if (proc.errorStream.bufferedReader().ready()) {
                    output += proc.errorStream.bufferedReader().readText().trim()
                }
            }
            else {
                output = proc.inputStream.bufferedReader().readText().trim() + proc.errorStream.bufferedReader().readText().trim()
            }
            proc.destroy()
            proc.waitFor()
            Log.debug("${commands.firstOrNull()} exited ${proc.exitValue()} with timeout")
            Pair(output, proc.exitValue())
        }
    }
    catch (e: IOException) {
        Log.debug(e.message ?: "Unknown IOException occurred")
        if (!returnExceptionMessage) {
            Pair(null, -1) // Don't print the stacktrace because that is confusing.
        }
        else {
            Pair(e.message, -1)
        }
    }
    catch (e: ProcessNotCreatedException) {
        Log.debug(e.message ?: "Unknown ProcessNotCreatedException occurred")
        // e.g. if the command is just trying if a program can be run or not, and it's not the case
        if (!returnExceptionMessage) {
            Pair(null, -1)
        }
        else {
            Pair(e.message, -1)
        }
    }
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil

object TexLivePackages {

    /**
     * List of installed packages.
     */
    var packageList: MutableList<String> = mutableListOf()

    /**
     * Given a package name used in \usepackage or \RequirePackage, find the
     * name needed to install from TeX Live. E.g. to be able to use \usepackage{rubikrotation}
     * we need to install the rubik package.
     *
     * In the output
     *
     *    tlmgr: package repository http://ctan.math.utah.edu/ctan/tex-archive/systems/texlive/tlnet (verified)
     *    rubik:
     *            texmf-dist/tex/latex/rubik/rubikrotation.sty
     *
     * we are looking for "rubik". Possibly tex live outputs a "TeX Live 2019 is frozen" message before, so
     * we search for the line that starts with tlmgr. Then the name of the package we are
     * looking for will be on the next line, if it exists.
     */
    fun findTexLiveName(task: Task.Backgroundable, packageName: String, project: Project): String? {
        // Find the package name for tlmgr.
        task.title = "Searching for $packageName..."
        val tlmgrExecutable = LatexSdkUtil.getExecutableName("tlmgr", project)
        // Assume that you can not use the bundle name in a \\usepackage if it is different from the package name (otherwise this search won't work and we would need to use tlmgr search --global $packageName
        val searchResult = "$tlmgrExecutable search --file --global /$packageName.sty".runCommand()
            ?: return null

        // Check if tlmgr needs to be updated first, and do so if needed.
        val tlmgrUpdateCommand = "$tlmgrExecutable update --self"
        if (searchResult.contains(tlmgrUpdateCommand)) {
            task.title = "Updating tlmgr..."
            tlmgrUpdateCommand.runCommand()
        }

        return extractRealPackageNameFromOutput(searchResult)
    }

    fun extractRealPackageNameFromOutput(output: String): String? {
        val tlFrozen = Regex(
            """
            TeX Live \d{4} is frozen forever and will no
            longer be updated\.\s+This happens in preparation for a new release\.

            If you're interested in helping to pretest the new release \(when
            pretests are available\), please read https://tug\.org/texlive/pretest\.html\.
            Otherwise, just wait, and the new release will be ready in due time\.
            """.trimIndent()
        )
        val lines = tlFrozen.replace(output, "").trim().split('\n')
        val tlmgrIndex = lines.indexOfFirst { it.startsWith("tlmgr:") }
        return try {
            lines[tlmgrIndex + 1].trim().dropLast(1) // Drop the : behind the package name.
        }
        catch (e: IndexOutOfBoundsException) {
            null
        }
    }
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.actionSystem.DataKey
import com.intellij.ui.treeStructure.Tree

object TexifyDataKeys {

    val LIBRARY_TREE = DataKey.create<Tree>("tree")

    val LIBRARY_IDENTIFIER = DataKey.create<String>("library.identifier")

    val LIBRARY_NAME = DataKey.create<String>("library.name")
}

package nl.hannahsten.texifyidea.util

import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.panels.HorizontalLayout
import com.intellij.util.ui.JBUI
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.KeyListener
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.text.JTextComponent

/**
 * Adds a text listener to the component.
 *
 * @param event
 *          The function to execute when any text updates in the component.
 * @return The document listener that was added to the component's document.
 */
fun JTextComponent.addTextChangeListener(event: (DocumentEvent?) -> Unit): DocumentListener {
    val documentListener = object : DocumentListener {
        override fun insertUpdate(e: DocumentEvent?) {
            event(e)
        }

        override fun removeUpdate(e: DocumentEvent?) {
            event(e)
        }

        override fun changedUpdate(e: DocumentEvent?) {
            event(e)
        }
    }
    document.addDocumentListener(documentListener)
    return documentListener
}

/**
 * Adds a key typed listener to the component.
 *
 * @param event
 *          The function to execute when any key is typed.
 * @return The key listener that was added to the component.
 */
fun JTextComponent.addKeyTypedListener(event: (KeyEvent) -> Unit): KeyListener {
    val adapter = object : KeyAdapter() {
        override fun keyTyped(e: KeyEvent?) {
            e?.let { event(it) }
        }
    }
    addKeyListener(adapter)
    return adapter
}

/**
 * Adds a key released listener to the component.
 *
 * @param event
 *          The function to execute when any key is released.
 * @return The key listener that was added to the component.
 */
fun JTextComponent.addKeyReleasedListener(event: (KeyEvent) -> Unit): KeyListener {
    val adapter = object : KeyAdapter() {
        override fun keyReleased(e: KeyEvent?) {
            e?.let { event(it) }
        }
    }
    addKeyListener(adapter)
    return adapter
}

/**
 * Only allows the given characters to be typed into the component.
 *
 * @return The KeyListener that is used for the filter.
 */
fun JTextComponent.setInputFilter(allowedCharacters: Set<Char>) = addKeyTypedListener {
    if (it.keyChar !in allowedCharacters) {
        it.consume()
    }
}

/**
 * Adds a component to the panel with a label before it.
 *
 * @param component
 *          The component to add to the panel.
 * @param description
 *          The label to put before the component.
 * @param labelWidth
 *          The fixed label width, or `null` to use the label's inherent size.
 */
fun JPanel.addLabeledComponent(
    component: JComponent,
    description: String,
    labelWidth: Int? = null,
    leftPadding: Int = 16
): JPanel {
    // Uses a border layout with West for the label and Center for the control itself.
    // East is reserved for suffix elements.
    val pane = JPanel(BorderLayout()).apply {
        val label = JBLabel(description).apply {
            // Left padding.
            border = JBUI.Borders.emptyLeft(leftPadding)

            // Custom width if specified.
            labelWidth?.let {
                preferredSize = Dimension(it, height)
            }

            // Align top.
            alignmentY = 0.0f
        }
        add(label, BorderLayout.WEST)
        add(component, BorderLayout.CENTER)
    }
    add(pane)
    return pane
}

/**
 * Creates a jpanel with horizontally aligned elements.
 */
fun hbox(spacing: Int = 8, vararg components: JComponent) = JPanel(HorizontalLayout(spacing)).apply {
    components.forEach { add(it) }
}

package nl.hannahsten.texifyidea.action.analysis

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.SystemEnvironment
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import nl.hannahsten.texifyidea.util.parser.*
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import java.io.File
import java.util.*
import java.util.regex.Pattern
import javax.swing.JLabel
import javax.swing.SwingConstants

/**
 * @author Hannah Schellekens
 */
open class WordCountAction : AnAction(
    "_Word Count",
    "Estimate the word count of the currently active .tex file and inclusions.",
    TexifyIcons.WORD_COUNT
) {

    companion object {

        /**
         * Commands that should be ignored by the word counter.
         */
        private val IGNORE_COMMANDS = setOf(
            "\\usepackage", "\\documentclass", "\\label", "\\linespread", "\\ref", "\\cite", "\\eqref", "\\nameref",
            "\\autoref", "\\fullref", "\\pageref", "\\newcounter", "\\newcommand", "\\renewcommand",
            "\\setcounter", "\\resizebox", "\\includegraphics", "\\include", "\\input", "\\refstepcounter",
            "\\counterwithins", "\\RequirePackage", "\\bibliography", "\\bibliographystyle"
        )

        /**
         * List of all environments that must be ignored.
         */
        private val IGNORE_ENVIRONMENTS = setOf(
            "tikzpicture", "thebibliography"
        )

        /**
         * Words that are contractions when `'s` is appended.
         */
        private val CONTRACTION_S = listOf(
            "that", "it", "there", "she", "he"
        )

        /**
         * Characters that serve as delimiters for contractions.
         */
        private val CONTRACTION_CHARACTERS = Pattern.compile("['’]")

        /**
         * Words containing solely of punctuation must be ignored.
         */
        private val PUNCTUATION = Pattern.compile("[.,\\-_–:;?!'\"~=+*/\\\\&|]+")
    }

    override fun actionPerformed(event: AnActionEvent) {
        val virtualFile = event.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return
        val project = event.getData(PlatformDataKeys.PROJECT) ?: return
        val psiFile = virtualFile.psiFile(project) ?: return

        // Prefer texcount, I think it is slightly more accurate
        val dialog = if (SystemEnvironment.isAvailable("texcount")) {
            val root = psiFile.findRootFile().virtualFile
            val (output, exitCode) = runCommandWithExitCode("texcount", "-1", "-inc", "-sum", root.name, workingDirectory = File(root.parent.path))
            if (exitCode == 0 && output?.toIntOrNull() != null) {
                makeDialog(psiFile, output.toInt())
            }
            else {
                // If there is an error, the output will contain both word count and error message (which could indicate a problem with the document itself)
                val words = "[0-9]+".toRegex().find(output ?: "")?.value
                makeDialog(psiFile, wordCount = words?.toIntOrNull(), errorMessage = output?.drop(words?.length ?: 0))
            }
        }
        else {
            val (words, chars) = countWords(psiFile)
            makeDialog(psiFile, words, chars)
        }

        dialog.show()
    }

    private fun formatAsHtml(type: String, message: String?): String {
        return if (message == null) {
            ""
        }
        else {
            "|
$type:	${message.take(5000)}
"
        }
    }

    /**
     * Builds the dialog that must show the word count.
     */
    private fun makeDialog(baseFile: PsiFile, wordCount: Int?, characters: Int? = null, errorMessage: String? = null): DialogBuilder {
        return DialogBuilder().apply {
            setTitle("Word Count")

            setCenterPanel(
                JLabel(
                    """|
                        |
Analysis of ${baseFile.name} (and inclusions):

                        |

                        ${formatAsHtml("Word count", wordCount?.toString())}
                        ${formatAsHtml("Characters", characters?.toString())}
                        ${formatAsHtml("Error message", errorMessage)}
                        |

                        |
                    """.trimMargin(),
                    AllIcons.General.InformationDialog,
                    SwingConstants.LEADING
                )
            )

            addOkAction()
            setOkOperation {
                dialogWrapper.close(0)
            }
        }
    }

    /**
     * Counts all the words in the given base file.
     */
    private fun countWords(baseFile: PsiFile): Pair<Int, Int> {
        val fileSet = baseFile.referencedFileSet()
            .filter { it.name.endsWith(".tex", ignoreCase = true) }
        val allNormalText = fileSet.flatMap { it.childrenOfType(LatexNormalText::class) }
        val parameterText = fileSet.flatMap { it.childrenOfType(LatexParameterText::class) }
            .filter {
                val commandText = it.command?.text ?: return@filter false
                return@filter commandText !in IGNORE_COMMANDS
            }

        val bibliographies = baseFile.childrenOfType(LatexEnvironment::class)
            .filter {
                val children = it.childrenOfType(LatexBeginCommand::class)
                if (children.isEmpty()) {
                    return@filter false
                }

                val parameters = children.first().parameterList
                if (parameters.isEmpty()) {
                    return@filter false
                }

                return@filter parameters[0].text == "{thebibliography}"
            }
        val bibliography = bibliographies.flatMap { it.childrenOfType(LatexNormalText::class) }

        val (wordsNormal, charsNormal) = countWords(allNormalText)
        val (wordsParameter, charsParameter) = countWords(parameterText)
        val (wordsBib, charsBib) = countWords(bibliography)

        return Pair(wordsNormal + wordsParameter - wordsBib, charsNormal + charsParameter - charsBib)
    }

    /**
     * Counts all the words in the text elements.
     *
     * @return A pair of the total amount of words, and the amount of characters that make up the words.
     */
    private fun countWords(latexNormalText: List<PsiElement>): Pair<Int, Int> {
        // separate all latex words.
        val latexWords: MutableSet<PsiElement> = HashSet()
        var characters = 0
        for (text in latexNormalText) {
            var child = text.firstChild
            while (child != null) {
                latexWords.add(child)
                child = child.nextSibling

                if (child is PsiWhiteSpace) {
                    characters += child.textLength
                    child = child.nextSibling
                    continue
                }
            }
        }

        // Count words.
        val filteredWords = filterWords(latexWords)

        var wordCount = 0
        for (word in filteredWords) {
            wordCount += contractionCount(word.text)
            characters += word.textLength
        }

        return Pair(wordCount, characters)
    }

    /**
     * Filters out all the words that should not be counted.
     */
    private fun filterWords(words: MutableSet<PsiElement>): Set<PsiElement> {
        val set: MutableSet<PsiElement> = HashSet()

        for (word in words) {
            if (isWrongCommand(word) || isOptionalParameter(word) || isEnvironmentMarker(word) || isPunctuation(word) ||
                isInWrongEnvironment(word) || isInMath(word)
            ) {
                continue
            }

            set.add(word)
        }

        return set
    }

    /**
     * Checks if the word is in inline math mode or not.
     */
    private fun isInMath(word: PsiElement): Boolean {
        return word.inMathContext()
    }

    /**
     * Checks if the given word is in an environment that must be ignored.
     */
    private fun isInWrongEnvironment(word: PsiElement): Boolean {
        return word.inDirectEnvironment(IGNORE_ENVIRONMENTS)
    }

    /**
     * `I've` counts for two words: `I` and `have`. This function gives you the number of original words.
     */
    private fun contractionCount(word: String): Int {
        val split = CONTRACTION_CHARACTERS.split(word)
        var count = 0
        for (i in split.indices) {
            val string = split[i]

            // Only count contractions: so do not count start or end single quotes :)
            if (string.isEmpty()) continue

            if (string.lowercase(Locale.getDefault()) == "s") {
                if (split.size == 1) return 1

                if (i > 0 && CONTRACTION_S.contains(split[i - 1].lowercase(Locale.getDefault()))) {
                    count++
                }
            }
            else count++
        }

        return count
    }

    private fun isPunctuation(word: PsiElement): Boolean {
        return PUNCTUATION.matcher(word.text).matches()
    }

    private fun isEnvironmentMarker(word: PsiElement): Boolean {
        val grandparent = word.grandparent(7)
        return grandparent is LatexBeginCommand || grandparent is LatexEndCommand
    }

    private fun isOptionalParameter(word: PsiElement): Boolean {
        return word.grandparent(5) is LatexOptionalParam
    }

    private fun isWrongCommand(word: PsiElement): Boolean {
        val command = word.grandparent(7) as? LatexCommands ?: return false

        return IGNORE_COMMANDS.contains(command.name)
    }
}

package nl.hannahsten.texifyidea.action.debug

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import nl.hannahsten.texifyidea.ui.symbols.tools.generateSymbolImages

/**
 * @author Hannah Schellekens
 */
@Suppress("ComponentNotRegistered", "unused") // Only for development purposes.
open class GenerateSymbolImagesAction : AnAction(
    "Generate Symbol Images",
    "(Development) Generates the symbol images for the symbol view",
    null
) {

    override fun actionPerformed(event: AnActionEvent) {
        val project = event.getData(PlatformDataKeys.PROJECT) ?: return

        val fileDescriptor = FileChooserDescriptor(false, true, false, false, false, false)
            .withTitle("Select Symbols Folder...")

        FileChooser.chooseFile(fileDescriptor, project, null) {
            generateSymbolImages(it.path)
            Notification("LaTeX", "Finished generating symbol images", "Generated symbol images to:
${it.path}", NotificationType.INFORMATION)
        }
    }
}

package nl.hannahsten.texifyidea.action.group

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens
 */
open class InsertFontStyleActionGroup : DefaultActionGroup() {

    override fun update(event: AnActionEvent) {
        super.update(event)
        event.presentation.icon = TexifyIcons.FONT_BOLD
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.action.group

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens
 */
open class InsertSectioningActionGroup : DefaultActionGroup() {

    override fun update(event: AnActionEvent) {
        super.update(event)
        event.presentation.icon = TexifyIcons.DOT_SECTION
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.action.group

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.hannahsten.texifyidea.util.files.isLatexFile

/**
 * @author Hannah Schellekens
 */
open class LatexEditMenuGroup : DefaultActionGroup() {

    override fun update(event: AnActionEvent) {
        val file = event.getData(CommonDataKeys.PSI_FILE)
        event.presentation.isEnabledAndVisible = file?.isLatexFile() ?: false
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertBoldfaceAction : InsertEditorAction("Bold face", TexifyIcons.FONT_BOLD, "\\textbf{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertChapterAction : InsertEditorAction("Chapter", TexifyIcons.DOT_CHAPTER, "\\chapter{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Adam Williams
 */
class InsertEmphasisAction : InsertEditorAction("Emphasis", null, "\\emph{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertItalicsAction : InsertEditorAction("Italics", TexifyIcons.FONT_ITALICS, "\\textit{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertOverlineAction : InsertEditorAction("Overline", TexifyIcons.FONT_OVERLINE, "\\overline{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertParagraphAction : InsertEditorAction("Paragraph", TexifyIcons.DOT_PARAGRAPH, "\\paragraph{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertPartAction : InsertEditorAction("Part", TexifyIcons.DOT_PART, "\\part{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertSectionAction : InsertEditorAction("Section", TexifyIcons.DOT_SECTION, "\\section{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertSlantedAction : InsertEditorAction("Slanted", TexifyIcons.FONT_SLANTED, "\\textsl{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertSmallCapsAction : InsertEditorAction("Small capitals", TexifyIcons.FONT_SMALLCAPS, "\\textsc{", "}")

package nl.hannahsten.texifyidea.action.insert

import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.ULEM
import nl.hannahsten.texifyidea.util.insertUsepackage

/**
 * @author Hannah Schellekens
 */
class InsertStrikethroughAction : InsertEditorAction(
    "Strikethrough (ulem package)",
    TexifyIcons.FONT_STRIKETHROUGH,
    "\\sout{", "}"
) {

    override fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor) {
        super.actionPerformed(file, project, textEditor)

        val psiFile = PsiManager.getInstance(project).findFile(file) ?: return

        runWriteAction(project, file) {
            psiFile.insertUsepackage(ULEM)
        }
    }
}

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertSubParagraphAction : InsertEditorAction("Subparagraph", TexifyIcons.DOT_SUBPARAGRAPH, "\\subparagraph{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertSubSectionAction : InsertEditorAction("Subsection", TexifyIcons.DOT_SUBSECTION, "\\subsection{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertSubSubSectionAction : InsertEditorAction("Subsubsection", TexifyIcons.DOT_SUBSUBSECTION, "\\subsubsection{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

class InsertTable(tableAsString: String? = "") :
    InsertEditorAction("Table", TexifyIcons.STATS, tableAsString, "") // The icon is never used.

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertTypewriterAction : InsertEditorAction("Typewriter (monospace)", TexifyIcons.FONT_TYPEWRITER, "\\texttt{", "}")

package nl.hannahsten.texifyidea.action.insert

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.InsertEditorAction

/**
 * @author Hannah Schellekens
 */
class InsertUnderlineAction : InsertEditorAction("Underline", TexifyIcons.FONT_UNDERLINE, "\\underline{", "}")

package nl.hannahsten.texifyidea.action.library

import arrow.core.Either
import arrow.core.getOrElse
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.progress.ProcessCanceledException
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 kotlinx.coroutines.runBlocking
import nl.hannahsten.texifyidea.RemoteLibraryRequestFailure
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.remotelibraries.RemoteBibLibrary
import nl.hannahsten.texifyidea.structure.bibtex.BibtexStructureViewEntryElement
import nl.hannahsten.texifyidea.ui.remotelibraries.AddLibDialogWrapper
import nl.hannahsten.texifyidea.ui.remotelibraries.LibraryMutableTreeNode
import nl.hannahsten.texifyidea.util.TexifyDataKeys
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel

/**
 * Action to add a remote library to the libraries tool window.
 *
 * To create an action to add a remote library, subclass this one and implement the abstract members. Then add that action
 * to plugin.xml.
 */
abstract class AddLibraryAction<Lib : RemoteBibLibrary, T : AddLibDialogWrapper> : AnAction() {

    /**
     * Add the elements from the library to the tree in the tool window.
     */
    override fun actionPerformed(e: AnActionEvent) {
        val dialogWrapper: T = getDialog(e.project ?: return)

        if (dialogWrapper.showAndGet()) {
            ProgressManager.getInstance()
                .run(object : Backgroundable(e.project, "Adding ${dialogWrapper.displayName} library") {
                    lateinit var library: Lib
                    lateinit var bibItems: List<BibtexEntry>

                    override fun run(indicator: ProgressIndicator) {
                        runBlocking {
                            // Cannot be destructured directly.
                            val (libraryT, bibItemsT) = createLibrary(dialogWrapper, e.project!!).getOrElse {
                                library.showNotification(e.project!!, it.libraryName, it.response)
                                // Apparently this is the way to cancel the task (and thus to avoid going into the onSuccess).
                                throw ProcessCanceledException()
                            } ?: return@runBlocking
                            library = libraryT
                            bibItems = bibItemsT
                        }
                    }

                    override fun onSuccess() {
                        val tree = e.getData(TexifyDataKeys.LIBRARY_TREE) ?: return
                        val model = tree.model as DefaultTreeModel
                        val root = model.root as DefaultMutableTreeNode

                        val libraryNode = LibraryMutableTreeNode(library.identifier, library.displayName)
                        bibItems.forEach { bib ->
                            val entryElement = BibtexStructureViewEntryElement(bib)
                            val entryNode = DefaultMutableTreeNode(entryElement)
                            libraryNode.add(entryNode)

                            // Each bib item has tags that show information, e.g., the author.
                            entryElement.children.forEach {
                                entryNode.add(DefaultMutableTreeNode(it))
                            }
                        }
                        root.add(libraryNode)
                        model.nodeStructureChanged(root)
                    }
                })
        }

        onFinish()
    }

    /**
     * Override this method to do something after the library has been added.
     */
    open fun onFinish() = Unit

    /**
     * Create the dialog that handles the logging in of the user.
     *
     * The elements of the library are added when the user clicks OK. When the user clicks Cancel, nothing happens.
     */
    abstract fun getDialog(project: Project): T

    /**
     * Get access to the user's online library, retrieve, store, and return the elements.
     */
    abstract suspend fun createLibrary(dialogWrapper: T, project: Project): Either<RemoteLibraryRequestFailure, Pair<Lib, List<BibtexEntry>>?>
}

package nl.hannahsten.texifyidea.action.library

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.ui.treeStructure.Tree
import nl.hannahsten.texifyidea.remotelibraries.RemoteBibLibraryFactory
import nl.hannahsten.texifyidea.remotelibraries.RemoteLibraryManager
import nl.hannahsten.texifyidea.ui.remotelibraries.findLibraryNode
import nl.hannahsten.texifyidea.util.TexifyDataKeys
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel

class RemoveLibraryAction : AnAction() {

    /**
     * Removing a library does the following:
     *
     * - Remove all credentials from the password safe.
     * - Remove the library from the [RemoteLibraryManager], this permanently erases the information stored on disk.
     * - Remove the library node (and its children) from the tree in the libraries tool window.
     */
    override fun actionPerformed(e: AnActionEvent) {
        val libraryKey = e.getData(TexifyDataKeys.LIBRARY_IDENTIFIER) ?: return
        RemoteBibLibraryFactory.fromStorage(libraryKey)?.destroyCredentials()
        RemoteLibraryManager.getInstance().removeLibraryByKey(libraryKey)

        val tree = e.getData(TexifyDataKeys.LIBRARY_TREE) as Tree
        val model = tree.model as DefaultTreeModel
        val root = model.root as DefaultMutableTreeNode
        root.remove(tree.findLibraryNode(libraryKey))
        model.nodeStructureChanged(root)
    }
}

package nl.hannahsten.texifyidea.action.library

import arrow.core.getOrElse
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.ui.treeStructure.Tree
import kotlinx.coroutines.runBlocking
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.remotelibraries.RemoteBibLibrary
import nl.hannahsten.texifyidea.remotelibraries.RemoteBibLibraryFactory
import nl.hannahsten.texifyidea.remotelibraries.RemoteLibraryManager
import nl.hannahsten.texifyidea.structure.bibtex.BibtexStructureViewEntryElement
import nl.hannahsten.texifyidea.ui.remotelibraries.LibraryMutableTreeNode
import nl.hannahsten.texifyidea.ui.remotelibraries.findLibraryNode
import nl.hannahsten.texifyidea.util.TexifyDataKeys
import nl.hannahsten.texifyidea.util.parser.getIdentifier
import java.util.*
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreePath

class SyncLibraryAction : AnAction() {

    /**
     * Synchronize the libraries that are selected in the library tree view. When there is no library selected - or the
     * user doesn't have the library tool window open - synchronize all the libraries.
     */
    override fun actionPerformed(e: AnActionEvent) {
        val project = e.project ?: return
        val libraries = RemoteBibLibraryFactory
            .fromStorage(e.getData(TexifyDataKeys.LIBRARY_IDENTIFIER))
            ?.let { listOf(it) }
            // Fallback to synchronizing all the libraries.
            ?: RemoteLibraryManager.getInstance().getLibraries()
                .map {
                    RemoteBibLibraryFactory.fromStorage(it.key)
                }

        libraries.filterNotNull().forEach { syncLibrary(it, project, e) }
    }

    private fun syncLibrary(library: RemoteBibLibrary, project: Project, e: AnActionEvent) {
        ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Syncing ${library.displayName}...") {
            lateinit var bibItems: List<BibtexEntry>
            lateinit var expandedPaths: Enumeration<TreePath>
            val tree: Tree? by lazy { e.getData(TexifyDataKeys.LIBRARY_TREE) }

            override fun run(indicator: ProgressIndicator) {
                runBlocking {
                    bibItems = library.getCollection().getOrElse {
                        library.showNotification(e.project!!, it.libraryName, it.response)
                        // Apparently this is the way to cancel the task (and thus to avoid going into the onSuccess).
                        throw ProcessCanceledException()
                    }
                    RemoteLibraryManager.getInstance().updateLibrary(library, bibItems)
                    expandedPaths =
                        tree?.let { it.getExpandedDescendants(TreePath(it.model.root)) } ?: return@runBlocking
                }
            }

            override fun onSuccess() {
                // If the tree is null the user hasn't opened the library tool window yet, so there is no tree to update.
                // The tree will be drawn with the updated library elements once the user opens the tool window.
                tree?.let {
                    ProgressManager.getInstance().run(object : Backgroundable(project, "Updating tree...") {
                        override fun run(indicator: ProgressIndicator) {
                            runReadAction {
                                val model = it.model as DefaultTreeModel
                                val root = model.root as? DefaultMutableTreeNode ?: return@runReadAction
                                val libraryNode: LibraryMutableTreeNode = it.findLibraryNode(library.identifier)
                                    ?: LibraryMutableTreeNode(library.identifier, library.displayName)
                                libraryNode.children().asSequence()
                                    .map { it as DefaultMutableTreeNode }
                                    .filter { (it.userObject as BibtexStructureViewEntryElement).entry.getIdentifier() !in bibItems.map { bib -> bib.getIdentifier() } }
                                    .forEach { libraryNode.remove(it) }

                                val itemsInLib = libraryNode.children().asSequence()
                                    .map { ((it as DefaultMutableTreeNode).userObject as BibtexStructureViewEntryElement).entry.getIdentifier() }
                                    .toList()
                                bibItems.forEach { bib ->
                                    if (bib.getIdentifier() !in itemsInLib) {
                                        val entryElement = BibtexStructureViewEntryElement(bib)
                                        val entryNode = DefaultMutableTreeNode(entryElement)
                                        libraryNode.add(entryNode)

                                        // Each bib item has tags that show information, e.g., the author.
                                        entryElement.children.forEach {
                                            entryNode.add(DefaultMutableTreeNode(it))
                                        }
                                    }
                                }

                                if (!root.children().contains(libraryNode)) root.add(libraryNode)
                                model.nodeStructureChanged(root)
                                expandedPaths.asIterator().forEach(it::expandPath)
                            }
                        }
                    })
                }
            }
        })
    }
}

fun <T> Enumeration<T>.contains(element: T): Boolean {
    while (hasMoreElements()) {
        if (nextElement() == element) return true
    }
    return false
}

package nl.hannahsten.texifyidea.action.preview

import javax.swing.JPanel

/**
 * @author Sergei Izmailov
 */
class EquationPreviewToolWindow {

    private val previewForm = PreviewForm()

    val content: JPanel?
        get() = previewForm.panel

    val form: PreviewForm
        get() = previewForm
}

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import nl.hannahsten.texifyidea.util.SystemEnvironment
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import java.io.File
import java.io.IOException
import java.io.PrintWriter
import java.nio.file.Paths
import javax.imageio.ImageIO
import javax.swing.SwingUtilities

/**
 * Preview based on Inkscape.
 */
class InkscapePreviewer : Previewer {

    override fun preview(input: String, previewForm: PreviewForm, project: Project, preamble: String, waitTime: Long) {
        runBlocking {
            launch {
                try {
                    // Snap apps are confined to the users home directory
                    if (SystemEnvironment.isInkscapeInstalledAsSnap) {
                        setPreviewCodeInTemp(
                            FileUtil.createTempDirectory(File(System.getProperty("user.home")), "preview", null),
                            input,
                            project,
                            preamble,
                            previewForm,
                            waitTime
                        )
                    }
                    else {
                        setPreviewCodeInTemp(FileUtil.createTempDirectory("preview", null), input, project, preamble, previewForm, waitTime)
                    }
                }
                catch (exception: AccessDeniedException) {
                    previewForm.setLatexErrorMessage("${exception.message}")
                }
                catch (exception: IOException) {
                    previewForm.setLatexErrorMessage("${exception.message}")
                }
            }
        }
    }

    /**
     * First define the function that actually does stuff in a temp folder. The usual temp directory might not be
     * accessible by inkscape (e.g., when inkscape is a snap), and using function we can specify an alternative
     * temp directory in case the usual fails.
     */
    private fun setPreviewCodeInTemp(
        tempDirectory: File,
        previewCode: String,
        project: Project,
        preamble: String,
        previewForm: PreviewForm,
        waitTime: Long
    ) {
        try {
            val tempBasename = Paths.get(tempDirectory.path.toString(), "temp").toString()
            val writer = PrintWriter("$tempBasename.tex", "UTF-8")

            val tmpContent =
                """\documentclass{article}
$preamble
\begin{document}
$previewCode
\end{document}"""
            writer.println(tmpContent)
            writer.close()

            val latexStdoutText = runPreviewFormCommand(
                LatexSdkUtil.getExecutableName("pdflatex", project),
                arrayOf(
                    "-interaction=nonstopmode",
                    "-halt-on-error",
                    "$tempBasename.tex"
                ),
                tempDirectory,
                waitTime,
                previewForm
            ) ?: return

            runInkscape(tempBasename, tempDirectory, waitTime, previewForm)
            val image = ImageIO.read(File("$tempBasename.png"))
            SwingUtilities.invokeLater {
                previewForm.setPreview(image, latexStdoutText)
            }
        }
        finally {
            // Delete all the created temp files in the default temp directory.
            tempDirectory.deleteRecursively()
        }
    }

    private fun runPreviewFormCommand(
        command: String,
        args: Array<String>,
        workDirectory: File,
        waitTime: Long,
        previewForm: PreviewForm
    ): String? {
        val result = runCommandWithExitCode(command, *args, workingDirectory = workDirectory, timeout = waitTime)

        if (result.second != 0) {
            previewForm.setLatexErrorMessage("$command exited with ${result.second}\n${result.first ?: ""}")
            return null
        }

        return result.first
    }

    /**
     * Run inkscape command to convert pdf to png, depending on the version of inkscape.
     */
    private fun runInkscape(tempBasename: String, tempDirectory: File, waitTime: Long, previewForm: PreviewForm) {
        // If 1.0 or higher
        if (SystemEnvironment.inkscapeMajorVersion >= 1) {
            runPreviewFormCommand(
                inkscapeExecutable(),
                arrayOf(
                    "$tempBasename.pdf",
                    "--export-area-drawing",
                    "--export-dpi", "1000",
                    "--export-background", "#FFFFFF",
                    "--export-background-opacity", "1.0",
                    "--export-filename", "$tempBasename.png"
                ),
                tempDirectory,
                waitTime,
                previewForm
            ) ?: throw AccessDeniedException(tempDirectory)
        }
        else {
            runPreviewFormCommand(
                pdf2svgExecutable(),
                arrayOf(
                    "$tempBasename.pdf",
                    "$tempBasename.svg"
                ),
                tempDirectory,
                waitTime,
                previewForm
            ) ?: return

            runPreviewFormCommand(
                inkscapeExecutable(),
                arrayOf(
                    "$tempBasename.svg",
                    "--export-area-drawing",
                    "--export-dpi", "1000",
                    "--export-background", "#FFFFFF",
                    "--export-png", "$tempBasename.png"
                ),
                tempDirectory,
                waitTime,
                previewForm
            ) ?: throw AccessDeniedException(tempDirectory)
        }
    }

    private fun inkscapeExecutable(): String {
        var suffix = ""
        if (SystemInfo.isWindows) {
            suffix = ".exe"
        }
        return "inkscape$suffix"
    }

    private fun pdf2svgExecutable(): String {
        var suffix = ""
        if (SystemInfo.isWindows) {
            suffix = ".exe"
        }
        return "pdf2svg$suffix"
    }
}

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.project.Project
import com.intellij.ui.Gray
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBUI
import org.apache.batik.dom.GenericDOMImplementation
import org.apache.batik.svggen.SVGGeneratorContext
import org.apache.batik.svggen.SVGGraphics2D
import org.apache.batik.transcoder.TranscoderException
import org.apache.batik.transcoder.TranscoderInput
import org.apache.batik.transcoder.TranscoderOutput
import org.apache.batik.transcoder.image.ImageTranscoder
import org.apache.batik.transcoder.image.PNGTranscoder
import org.scilab.forge.jlatexmath.DefaultTeXFont
import org.scilab.forge.jlatexmath.ParseException
import org.scilab.forge.jlatexmath.TeXConstants
import org.scilab.forge.jlatexmath.TeXFormula
import org.scilab.forge.jlatexmath.cyrillic.CyrillicRegistration
import org.scilab.forge.jlatexmath.greek.GreekRegistration
import java.awt.Dimension
import java.io.*
import java.nio.file.Paths
import javax.imageio.ImageIO
import javax.swing.JLabel
import javax.swing.SwingUtilities

class JlatexmathPreviewer : Previewer {

    override fun preview(input: String, previewForm: PreviewForm, project: Project, preamble: String, waitTime: Long) {
        val tempBaseName = Paths.get(System.getProperty("java.io.tmpdir"), "temp").toString()
        try {
            // Fonts should be shapes, otherwise the user would have needed to install the font to render it
            // when converting to png (because fonts are not saved in svg).
            toSVG(input, tempBaseName, fontAsShapes = true)
            saveSvgAsPng(tempBaseName)
            val image = ImageIO.read(File("$tempBaseName.png"))
            SwingUtilities.invokeLater {
                previewForm.setPreview(image, "")
            }
        }
        catch (e: ParseException) {
            previewForm.setLatexErrorMessage(e.message ?: "There was an unknown problem with compiling the preview.")
        }
        catch (e: FileNotFoundException) {
            previewForm.setLatexErrorMessage(e.message ?: "There was an internal problem with JLatexmath")
        }
    }

    @Suppress("SameParameterValue")
    @Throws(FileNotFoundException::class, TranscoderException::class, IOException::class)
    private fun saveSvgAsPng(tempBaseName: String) {
        // Make sure image resolution is good
        // https://stackoverflow.com/a/63684055/4126843
        // Pick an arbitrary resolution (higher is slower)
        val resolutionDpi = 200

        // Define image width and height
        // We have to do some magic with scaling
        val scaleByResolution = resolutionDpi / 72f
        // Unfortunately, this is hardcoded for now, as we don't yet know the with of the to-be-created image panel?
        val scaledWidth = 300 * scaleByResolution
        val scaledHeight = 50 * scaleByResolution

        // Convert pixels per inch to mm per pixels
        val inch = 25.4f // mm
        val pixelUnitToMM = inch / resolutionDpi

        val transcoder = PNGTranscoder()
        val transcoderInput = TranscoderInput(FileInputStream("$tempBaseName.svg"))
        val os = FileOutputStream("$tempBaseName.png")
        val transcoderOutput = TranscoderOutput(os)
        transcoder.addTranscodingHint(ImageTranscoder.KEY_BACKGROUND_COLOR, JBColor.WHITE)
        transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, scaledWidth)
        transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, scaledHeight)

        transcoder.addTranscodingHint(PNGTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER, pixelUnitToMM)

        transcoder.transcode(transcoderInput, transcoderOutput)

        os.flush()
        os.close()
    }

    /**
     * https://github.com/opencollab/jlatexmath/blob/af77a8e80d41ff67dfe2f42f14b41f6860dfeeec/jlatexmath-example-export/src/test/java/org/scilab/forge/jlatexmath/examples/export/Convert.java#L39
     */
    @Throws(IOException::class)
    fun toSVG(latex: String?, tempBaseName: String, fontAsShapes: Boolean) {
        val domImpl = GenericDOMImplementation.getDOMImplementation()
        val svgNS = "http://www.w3.org/2000/svg"
        val document = domImpl.createDocument(svgNS, "svg", null)
        val ctx = SVGGeneratorContext.createDefault(document)
        val g2 = SVGGraphics2D(ctx, fontAsShapes)
        DefaultTeXFont.registerAlphabet(CyrillicRegistration())
        DefaultTeXFont.registerAlphabet(GreekRegistration())
        val formula = TeXFormula(latex)
        val icon = formula.createTeXIcon(TeXConstants.STYLE_DISPLAY, 20f)
        icon.insets = JBUI.insets(5)
        g2.svgCanvasSize = Dimension(icon.iconWidth, icon.iconHeight)
        g2.color = JBColor.WHITE
        g2.fillRect(0, 0, icon.iconWidth, icon.iconHeight)
        val jl = JLabel()
        jl.foreground = Gray._0
        icon.paintIcon(jl, g2, 0, 0)
        val useCSS = true
        val svgs = FileOutputStream("$tempBaseName.svg")
        val out: Writer = OutputStreamWriter(svgs, "UTF-8")
        g2.stream(out, useCSS)
        svgs.flush()
        svgs.close()
    }
}

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.ui.content.ContentFactory
import nl.hannahsten.texifyidea.action.EditorAction
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import java.util.*
import javax.swing.Icon

/**
 * The [PreviewAction] class is a wrapper around an action that displays a preview to the user.
 *
 * This preview is pretty general, you can display pretty much any part of the user's document, depending
 * on the goal of the implementation. This class has been used to create both the [ShowEquationPreview] Action
 * and the [ShowTikzPreview] Action for example.
 *
 * To create a new Preview Action, simply extend this class and call the [displayPreview] function
 * when appropriate.
 *
 * @author Sergei Izmailov
 * @author FalseHonesty
 */
abstract class PreviewAction(name: String, val icon: Icon?) : EditorAction(name, icon) {

    /**
     * This function is used to display the preview requested as the name suggests.
     *
     * In order to configure the minimal document produced, pass in a lambda to [config].
     * There you can change the document's preamble and content easily.
     *
     * @see [ShowTikzPreview.actionPerformed]
     */
    protected fun displayPreview(
        project: Project,
        element: PsiElement,
        key: Key<PreviewFormUpdater>,
        config: PreviewFormUpdater.() -> Unit
    ) {
        val toolWindowId = name
        val toolWindowManager = ToolWindowManager.getInstance(project)

        // Avoid adding it twice
        // Note: the plugin verifier claims this is Internal api, but it's not:
        // https://intellij-support.jetbrains.com/hc/en-us/community/posts/11533368171026-Registering-a-tool-window-programmatically
        val toolWindow = toolWindowManager.getToolWindow(toolWindowId) ?: toolWindowManager.registerToolWindow(toolWindowId) {
            contentFactory = PreviewToolWindowFactory()
            icon = this@PreviewAction.icon
        }

        val containingFile = element.containingFile
        val psiDocumentManager = PsiDocumentManager.getInstance(project)
        val document = psiDocumentManager.getDocument(containingFile)
        val textOffset = element.textOffset
        val lineNumber = document?.getLineNumber(textOffset) ?: 0
        val displayName = "${containingFile.name}:${lineNumber + 1}"

        val contentCount = toolWindow.contentManager.contentCount

        var replaced = false
        for (i in 0 until contentCount) {
            val content = toolWindow.contentManager.getContent(i) ?: continue
            if (!content.isPinned) {
                val form = content.getUserData(key) ?: continue
                form.config()
                form.compilePreview(element.text, project)
                content.displayName = displayName
                replaced = true
                break
            }
        }

        val contentFactory = ContentFactory.getInstance()

        if (!replaced) {
            val previewToolWindow = EquationPreviewToolWindow()
            val newContent = contentFactory.createContent(
                previewToolWindow.content,
                displayName,
                true
            )
            toolWindow.contentManager.addContent(newContent)
            val updater = PreviewFormUpdater(previewToolWindow.form)

            updater.config()

            newContent.putUserData(key, updater)
            updater.compilePreview(element.text, project)
        }
        // Show but not focus the window
        toolWindow.activate(null, false)
    }

    fun findPreamblesFromMagicComments(psiFile: PsiFile, name: String): String {
        val fileset = psiFile.referencedFileSet()
        val preambleRegex =
            """%! preview preamble\s*=\s*$name""".toRegex(EnumSet.of(RegexOption.IGNORE_CASE))
        val preambleRegexBeginEnd =
            """
                %! begin preamble\s*=\s*$name(?<content>(\n.*)*\n)%! end preamble\s*=\s*$name
            """.trimIndent().toRegex(EnumSet.of(RegexOption.IGNORE_CASE))

        var preamble = ""

        for (f in fileset) {
            if (preambleRegex.containsMatchIn(f.text)) {
                preamble += f.text
            }
            else {
                preambleRegexBeginEnd.findAll(f.text).forEach {
                    preamble += it.groups["content"]?.value?.trim()
                        ?: return@forEach
                }
            }
        }

        return preamble
    }
}

package nl.hannahsten.texifyidea.action.preview

import nl.hannahsten.texifyidea.ui.ImagePanel
import java.awt.image.BufferedImage
import javax.swing.JPanel
import javax.swing.JTabbedPane
import javax.swing.JTextArea

/**
 * @author Sergei Izmailov
 */
class PreviewForm {

    var panel: JPanel? = null
    private var equationArea: JTextArea? = null
    private var outputArea: JTextArea? = null
    private var equationPanel: ImagePanel? = null
    private var latexOutputTab: JPanel? = null
    private var equationTab: JPanel? = null
    private var tabbedPane: JTabbedPane? = null

    fun setEquation(equation: String) {
        equationArea!!.text = equation
    }

    fun setPreview(image: BufferedImage, latexOutput: String) {
        equationPanel!!.setImage(image)
        outputArea!!.text = latexOutput
        tabbedPane!!.selectedIndex = tabbedPane!!.indexOfComponent(equationTab)
    }

    fun setLatexErrorMessage(errorMessage: String) {
        outputArea!!.text = errorMessage
        equationPanel!!.clearImage()
        tabbedPane!!.selectedIndex = tabbedPane!!.indexOfComponent(latexOutputTab)
    }

    fun clear() {
        outputArea!!.text = ""
        equationArea!!.text = ""
        equationPanel!!.clearImage()
    }
}

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.project.Project

/**
 * @author Sergei Izmailov
 */
class PreviewFormUpdater(private val previewForm: PreviewForm) {

    /**
     * The default preamble.
     *
     * Unless you are going to set your own \pagestyle{}, simply append to this variable.
     */
    private val defaultPreamble =
        """
        \pagestyle{empty}
        """.trimIndent()

    /**
     * Preamble as specified by TeXiFy.
     * Only used if userPreamble is not empty.
     */
    var preamble = defaultPreamble

    /**
     * Preamble as specified by the user.
     * Modify this variable to include more packages.
     */
    var userPreamble = ""

    /**
     * Controls how long (in seconds) we will wait for the document compilation. If the time taken exceeds this,
     * we will return an error and not output a preview.
     */
    var waitTime = 3L

    /**
     * Reset the preamble to the default preamble.
     */
    fun resetPreamble() {
        preamble = defaultPreamble
        userPreamble = ""
    }

    /**
     * Sets the code that will be previewed, whether that be an equation, a tikz picture, or whatever else
     * you are trying to preview.
     *
     * This function also starts the creation and compilation of the temporary preview document, and will then
     * either display the preview, or if something failed, the error produced.
     */
    fun compilePreview(previewCode: String, project: Project) {
        previewForm.setEquation(previewCode)

        // Combine default and user defined preamble. Cannot be used if we decide to run latexmath.
        val preamble = preamble + userPreamble

        // jlatexmath cannot handle a custom preamble
        val previewer = if (userPreamble.isBlank()) {
            JlatexmathPreviewer()
        }
        else {
            InkscapePreviewer()
        }
        previewer.preview(previewCode, previewForm, project, preamble, waitTime)
    }
}

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory

class PreviewToolWindowFactory : ToolWindowFactory, DumbAware {

    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        // Default tool window content
    }
}

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.project.Project

/**
 * Can compile LaTeX into a preview image.
 */
interface Previewer {

    /**
     * Given the LaTeX input, construct an image and update the form, with image, output or error message.
     */
    fun preview(input: String, previewForm: PreviewForm, project: Project, preamble: String, waitTime: Long)
}

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.util.parser.findOuterMathEnvironment

/**
 * Show a preview of a LaTeX equation in a separate window.
 *
 * @author Sergei Izmailov
 */
class ShowEquationPreview : PreviewAction("Equation Preview", TexifyIcons.EQUATION_PREVIEW) {

    companion object {

        @JvmStatic
        val FORM_KEY = Key<PreviewFormUpdater>("updater")

        @JvmStatic
        val MATH_PREAMBLE = "\\usepackage{amsmath,amsthm,amssymb,amsfonts}"
    }

    override fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor) {
        val element: PsiElement = getElement(file, project, textEditor) ?: return

        val outerMathEnvironment = element.findOuterMathEnvironment() ?: return

        displayPreview(project, outerMathEnvironment, FORM_KEY) {
            resetPreamble()
            preamble += MATH_PREAMBLE
            val psiFile = element.containingFile
            userPreamble += findPreamblesFromMagicComments(psiFile, "math")
        }
    }
}

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.PackageUtils
import nl.hannahsten.texifyidea.util.parser.environmentName
import nl.hannahsten.texifyidea.util.parser.hasParent
import nl.hannahsten.texifyidea.util.parser.parentOfType
import java.util.*

/**
 * The [ShowTikzPreview] class describes an Action in the editor that will display a rendered
 * TikZ picture using the [PreviewAction] class as a base.
 *
 * @author FalseHonesty
 */
class ShowTikzPreview : PreviewAction("Tikz Picture Preview", TexifyIcons.TIKZ_PREVIEW) {

    companion object {

        @JvmStatic
        val FORM_KEY = Key<PreviewFormUpdater>("updater")
    }

    override fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor) {
        val element: PsiElement = getElement(file, project, textEditor)
            ?: return

        // Make sure we're currently in a tikz environment.
        val tikzEnvironment = findTikzEnvironment(element) ?: return

        displayPreview(project, tikzEnvironment, FORM_KEY) {
            resetPreamble()
            val psiFile = getPsiFile(file, project) ?: return@displayPreview

            preamble += "\\usepackage{tikz, pgfplots, amsmath}\n"

            // Add all the tikz libs included in related packages (via \\usetikzlibrary{}) to the produced document.
            val tikzLibs = PackageUtils.getIncludedTikzLibraries(psiFile)
            if (tikzLibs.isNotEmpty()) {
                userPreamble += "\\usetikzlibrary{${tikzLibs.joinToString()}}\n"
            }

            // Add all the pgfplots libs included in related packages (via \\usepgfplotslibrary{}) to the produced document.
            val pgfLibs = PackageUtils.getIncludedPgfLibraries(psiFile)
            if (pgfLibs.isNotEmpty()) {
                userPreamble += "\\usepgfplotslibrary{${pgfLibs.joinToString()}}\n"
            }

            userPreamble += findPreamblesFromMagicComments(psiFile, "tikz")
            waitTime = 5L
        }
    }

    fun findTikzEnvironment(innerElement: PsiElement): PsiElement? {
        // If the selected element is already a tikz env, we're good.
        if (innerElement is LatexEnvironment && innerElement.isTikz()) return innerElement

        // Find the first LatexEnvironment parent. If there are none, we aren't in tikz.
        var currElement = innerElement.parentOfType(LatexEnvironment::class)
            ?: return null

        // Continue this process until we find a tikz environment or run out of parent environments.
        while (!currElement.isTikz() && currElement.hasParent(LatexEnvironment::class)) {
            currElement = currElement.parentOfType(LatexEnvironment::class)!!
        }

        // Finally, decide whether the outermost environment is tikz.
        return if (currElement.isTikz()) currElement else null
    }

    private fun LatexEnvironment.isTikz() = beginCommand.environmentName()
        ?.lowercase(Locale.getDefault()) == "tikzpicture"
}

package nl.hannahsten.texifyidea.action.reformat

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessNotCreatedException
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.runWriteCommandAction
import java.util.concurrent.TimeUnit

/**
 * An action which performs a reformat of the currently open file using a certain external tool.
 *
 * @author Thomas
 */
abstract class ExternalReformatAction(title: String, val isValidFile: (file: PsiFile) -> Boolean) : AnAction(title), DumbAware {

    override fun update(event: AnActionEvent) {
        // Possible improvement: make visible anyway in LaTeX project (but only enabled if cursor in file)

        val presentation = event.presentation
        val dataContext = event.dataContext
        val project = CommonDataKeys.PROJECT.getData(dataContext)
        val editor = CommonDataKeys.EDITOR.getData(dataContext)
        if (project == null || editor == null) {
            presentation.isEnabledAndVisible = false
            return
        }

        // Only show when in a valid file
        val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document)
        if (file == null || file.virtualFile == null || (!isValidFile(file))) {
            presentation.isEnabledAndVisible = false
            return
        }
    }

    /**
     * Command to run the formatter.
     * Working directory will be the file's parent.
     *
     * Ideally, you use the file content from the psi tree, because when someone edits the file and directly
     * reformats afterwards, the file is most probably not yet saved to disk, so we have to do that ourselves.
     *
     * @param file File to reformat.
     */
    abstract fun getCommand(file: PsiFile): List<String>

    override fun actionPerformed(event: AnActionEvent) {
        val dataContext = event.dataContext
        val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return
        val editor = CommonDataKeys.EDITOR.getData(dataContext) ?: return
        val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
        if (!isValidFile(file)) return
        val command = getCommand(file)
        val process = try {
            GeneralCommandLine(command)
                .withWorkDirectory(file.containingDirectory.virtualFile.path)
                .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
                .createProcess()
        }
        catch (e: ProcessNotCreatedException) {
            Notification("LaTeX", "Could not run ${command.first()}", "Please double check if ${command.first()} is installed correctly: ${e.message}", NotificationType.ERROR).notify(project)
            return
        }
        val output = if (process.waitFor(3, TimeUnit.SECONDS)) {
            process.inputStream.bufferedReader().readText().trim() + process.errorStream.bufferedReader().readText().trim()
        }
        else {
            process.destroy()
            process.waitFor()
            null
        }
        if (process.exitValue() != 0) {
            Notification("LaTeX", "${command.first()} failed", output ?: "Exit value ${process.exitValue()}", NotificationType.ERROR).notify(project)
        }
        else if (output?.isNotBlank() == true) {
            processOutput(output, file, project)
        }
    }

    /**
     * Do whatever you want with the output of the program.
     */
    open fun processOutput(output: String, file: PsiFile, project: Project) {}

    /**
     * Replace the file content with the given output.
     * This is the preferred way, because it has immediate user feedback.
     * If you just modify the file content in the background, only on re-focus will the user be asked if he wants to see the external changes made.
     */
    fun replaceLatexFileContent(output: String, file: PsiFile, project: Project) {
        // replaceChild does not like Windows line separators
        val newFile = LatexPsiHelper(project).createFromText(output.replace("\r\n", "\n"))
        runWriteCommandAction(project) {
            file.node.replaceChild(file.node.firstChildNode, newFile.node.firstChildNode)
        }
    }

    /**
     * See [replaceLatexFileContent].
     */
    fun replaceBibtexFileContent(output: String, file: PsiFile, project: Project) {
        val newFile = PsiFileFactory.getInstance(project).createFileFromText("DUMMY.bib", BibtexLanguage, output, false, true)
        runWriteCommandAction(project) {
            file.node.replaceAllChildrenToChildrenOf(newFile.node)
        }
    }
}

package nl.hannahsten.texifyidea.action.reformat

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.BibtexFileType
import nl.hannahsten.texifyidea.util.runCommand
import org.apache.maven.artifact.versioning.DefaultArtifactVersion

/**
 * Run external tool 'bibtex-tidy' to reformat the file.
 * This action is placed next to the standard Reformat action in the Code menu.
 *
 * @author Thomas
 */
class ReformatWithBibtexTidy : ExternalReformatAction("Reformat File with bibtex-tidy", { it.fileType == BibtexFileType }) {

    companion object {

        val bibtexTidyVersion by lazy { DefaultArtifactVersion(runCommand("bibtex-tidy", "-v") ?: "0.0.0") }
    }

    override fun getCommand(file: PsiFile): List<String> {
        // Outputting to stdout was introduced in 1.7.2
        return if (bibtexTidyVersion < DefaultArtifactVersion("v1.7.2")) {
            listOf("bibtex-tidy", file.name, "--no-backup")
        }
        else {
            // We have to use shell in order to make the pipe work
            // We have to use a special string to escape all the single quotes
            listOf("/bin/sh", "-c", "echo '${file.text.replace("'", "'\"'\"'")}' | bibtex-tidy -")
        }
    }

    override fun processOutput(output: String, file: PsiFile, project: Project) {
        if (bibtexTidyVersion < DefaultArtifactVersion("v1.7.2")) return
        // Otherwise, we can use the stdout output
        replaceBibtexFileContent(output, file, project)
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.action.reformat

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.util.execution.ParametersListUtil
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.files.isLatexFile

/**
 * Run external tool 'latexindent.pl' to reformat the file.
 * This action is placed next to the standard Reformat action in the Code menu.
 *
 * @author Thomas
 */
class ReformatWithLatexindent : ExternalReformatAction("Reformat File with Latexindent", { it.isLatexFile() }) {

    override fun getCommand(file: PsiFile): List<String> {
        // Ensure the document is saved, before we run anything on it
        val document = PsiDocumentManager.getInstance(file.project).getDocument(file)
        if (document != null) {
            FileDocumentManager.getInstance().saveDocument(document)
        }
        // latexindent as command seems to work more often than latexindent.pl
        return listOf("latexindent") + ParametersListUtil.parse(TexifySettings.getInstance().latexIndentOptions) + listOf(file.name)
    }

    override fun processOutput(output: String, file: PsiFile, project: Project) {
        replaceLatexFileContent(output, file, project)
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.action.skim

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer
import nl.hannahsten.texifyidea.ui.SkimConfigureInverseSearchDialog

/**
 * @author Stephan Sundermann
 */
class ConfigureInverseSearchAction : AnAction() {

    override fun actionPerformed(e: AnActionEvent) {
        SkimConfigureInverseSearchDialog()
    }

    /**
     * Hide this option when Skim is not available.
     */
    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = InternalPdfViewer.SKIM.isAvailable()
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.action.sumatra

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.ui.DialogBuilder
import nl.hannahsten.texifyidea.run.sumatra.SumatraAvailabilityChecker
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import javax.swing.JLabel
import javax.swing.SwingConstants

/**
 * Sets up inverse search integration with SumatraPDF.
 *
 * This action attempts to set the permanent inverse search (backward search) command setting in SumatraPDF. This action is Windows-only.
 *
 * @author Sten Wessel
 * @since b0.4
 */
open class ConfigureInverseSearchAction : AnAction(
    "_Configure Inverse Search",
    "Setup inverse search integration with SumatraPDF and TeXiFy IDEA.",
    null
) {

    override fun actionPerformed(e: AnActionEvent) {
        DialogBuilder().apply {
            setTitle("Configure Inverse Search")
            setCenterPanel(
                JLabel(
                    "To enable inverse search (from PDF to source file), the inverse search setting in SumatraPDF must be changed.
" +
                        "By clicking OK, this change will automatically be applied and SumatraPDF will be restarted.

" +
                        "Warning: this will permanently overwrite the previous inverse search setting in SumatraPDF.",
                    AllIcons.General.WarningDialog,
                    SwingConstants.LEADING
                )
            )

            addOkAction()
            addCancelAction()
            setOkOperation {
                // First kill Sumatra to avoid having two instances open of which only one has the correct setting
                Runtime.getRuntime().exec("taskkill /IM SumatraPDF.exe")

                val path = PathManager.getBinPath()
                var name = ApplicationNamesInfo.getInstance().scriptName
                val sumatraWorkingDir = SumatraAvailabilityChecker.sumatraDirectory

                // If we can find a 64-bits Java, then we can start (the equivalent of) idea64.exe since that will use the 64-bits Java
                // see issue 104 and https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/809
                // If we find a 32-bits Java or nothing at all, we will keep (the equivalent of) idea.exe which is the default
                if (System.getProperty("sun.arch.data.model") == "64") {
                    // We will assume that since the user is using a 64-bit IDEA that name64 exists, this is at least true for idea64.exe and pycharm64.exe on Windows
                    name += "64"
                    // We also remove an extra "" because it opens an empty IDEA instance when present
                    runCommandWithExitCode("cmd.exe", "/C", "start", "SumatraPDF", "-inverse-search", "\"$path\\$name.exe\" --line %l \"%f\"", workingDirectory = sumatraWorkingDir, nonBlocking = true)
                }
                else {
                    runCommandWithExitCode("cmd.exe", "/C", "start", "SumatraPDF", "-inverse-search", "\"$path\\$name.exe\" \"\" --line %l \"%f\"", workingDirectory = sumatraWorkingDir, nonBlocking = true)
                }

                dialogWrapper.close(0)
            }

            show()
        }
    }

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = SumatraAvailabilityChecker.isSumatraAvailable
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder

/**
 * @author Hannah Schellekens
 */
open class CompositeHandler<T : LookupElement>(vararg val handlers: InsertHandler<T>) : InsertHandler<T> {

    override fun handleInsert(context: InsertionContext, lookupElement: T) {
        handlers.forEach { it.handleInsert(context, lookupElement) }
    }
}

/**
 * Adds multiple insert handlers to the LookupElementBuilder.
 */
fun LookupElementBuilder.withInsertHandlers(vararg handlers: InsertHandler<LookupElement>): LookupElementBuilder {
    return withInsertHandler(CompositeHandler(*handlers))
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.removeFileExtension
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * @author Hannah Schellekens
 */
open class FileNameInsertionHandler : InsertHandler<LookupElement> {

    override fun handleInsert(context: InsertionContext, element: LookupElement) {
        val text = element.`object`
        val file = context.file
        val document = file.document() ?: return
        val offset = context.startOffset
        val normalTextWord = file.findElementAt(offset) ?: return
        val command = normalTextWord.parentOfType(LatexCommands::class) ?: return

        if (command.name !in CommandMagic.illegalExtensions.keys) return

        val extensionless = text.toString().removeFileExtension()
        document.replaceString(offset, context.tailOffset, extensionless)
    }
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TextExpression
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import nl.hannahsten.texifyidea.lang.commands.Argument
import nl.hannahsten.texifyidea.lang.commands.RequiredArgument
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * @author Hannah Schellekens
 */
class LatexCommandArgumentInsertHandler(val arguments: List<Argument>? = null) : InsertHandler<LookupElement> {

    override fun handleInsert(insertionContext: InsertionContext, lookupElement: LookupElement) {
        insert(insertionContext, lookupElement)
    }

    private fun insert(context: InsertionContext, lookupElement: LookupElement) {
        val editor = context.editor
        val document = editor.document
        val caret = editor.caretModel
        val offset = caret.offset
        // When not followed by { or [ (whichever the first parameter starts with) insert the parameters.
        if (arguments != null && (
            offset >= document.textLength - 1 || document.getText(TextRange.from(offset, 1)) !in setOf("{", "[")
            )
        ) {
            insertParametersLiveTemplate(editor)
        }
        else {
            skipParameters(editor, lookupElement)
        }
    }

    private fun insertParametersLiveTemplate(editor: Editor) {
        // arguments is not null, we checked when calling this function.
        val template = TemplateImpl(
            "",
            arguments!!.mapIndexed { index: Int, argument: Argument ->
                if (argument is RequiredArgument) "{\$__Variable$index\$}" else "[\$__Variable$index\$]"
            }.joinToString(""),
            ""
        )
        repeat(arguments.size) { template.addVariable(TextExpression(""), true) }
        TemplateManager.getInstance(editor.project).startTemplate(editor, template)
    }

    private fun skipParameters(editor: Editor, lookupElement: LookupElement) {
        val document = editor.document
        val file = document.psiFile(editor.project ?: return)
        val caret = editor.caretModel
        val extraSpaces = lookupElement.lookupString.takeLastWhile { it == ' ' }.length
        val psiElement = file?.findElementAt(caret.offset + extraSpaces) ?: return
        if (psiElement.text in setOf("{", "[")) {
            caret.moveToOffset((psiElement.parentOfType(LatexCommands::class)?.endOffset() ?: return) - extraSpaces)
        }
    }
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.util.insertUsepackage

/**
 * @author Hannah Schellekens
 */
class LatexCommandPackageIncludeHandler : InsertHandler<LookupElement> {

    override fun handleInsert(insertionContext: InsertionContext, item: LookupElement) {
        val command = item.`object` as LatexCommand
        insertionContext.file.insertUsepackage(command.dependency)
    }
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import nl.hannahsten.texifyidea.lang.commands.Argument

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexMathInsertHandler(val arguments: List<Argument>?) : InsertHandler<LookupElement> {

    override fun handleInsert(context: InsertionContext, item: LookupElement) {
        LatexCommandArgumentInsertHandler(arguments).handleInsert(context, item)
        LatexCommandPackageIncludeHandler().handleInsert(context, item)
        RightInsertHandler().handleInsert(context, item)
    }
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingListener
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.codeInsight.template.impl.TextExpression
import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.lang.commands.Argument
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.lang.commands.RequiredArgument
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.definitionsAndRedefinitionsInFileSet
import nl.hannahsten.texifyidea.util.magic.TypographyMagic
import nl.hannahsten.texifyidea.util.parser.isEnvironmentDefinition
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexNoMathInsertHandler(val arguments: List<Argument>? = null) : InsertHandler<LookupElement> {

    override fun handleInsert(context: InsertionContext, item: LookupElement) {
        removeWhiteSpaces(context)
        val command = item.`object` as LatexCommand

        when (command.command) {
            LatexGenericRegularCommand.BEGIN.command -> {
                insertBegin(context)
            }
            in TypographyMagic.pseudoCodeBeginEndOpposites -> {
                insertPseudocodeEnd(command.command, context)
            }
            else -> {
                LatexCommandArgumentInsertHandler(arguments).handleInsert(context, item)
            }
        }

        RightInsertHandler().handleInsert(context, item)
        LatexCommandPackageIncludeHandler().handleInsert(context, item)
    }

    private fun insertPseudocodeEnd(name: String, context: InsertionContext) {
        val numberRequiredArguments = LatexCommand.lookup(name)
            ?.first()?.arguments
            ?.count { it is RequiredArgument } ?: 0

        val templateText = List(numberRequiredArguments) {
            "{\$__Variable${it}\$}"
        }.joinToString("") + "\n\$END\$\n\\${TypographyMagic.pseudoCodeBeginEndOpposites[name]}"
        val parameterTemplate = object : TemplateImpl("", templateText, "") {
            override fun isToReformat(): Boolean = false
        }
        repeat(numberRequiredArguments) { parameterTemplate.addVariable(TextExpression(""), true) }

        TemplateManager.getInstance(context.project)
            .startTemplate(context.editor, parameterTemplate)
    }

    /**
     * Inserts a live template to make the end command match the begin command.
     */
    private fun insertBegin(context: InsertionContext) {
        val templateText = "{\$__Variable0\$}\$END\$\n\\end{\$__Variable0\$}"
        val template = object : TemplateImpl("", templateText, "") {
            override fun isToReformat(): Boolean = true
        }
        template.addVariable(TextExpression(""), true)

        TemplateManager.getInstance(context.project)
            .startTemplate(context.editor, template, EnvironmentInsertImports(context))
    }

    /**
     * Insert a live template for the required arguments. When there are  no required
     * arguments, move to the content of the environment.
     */
    private fun insertRequiredArguments(environment: Environment?, context: InsertionContext) {
        val numberRequiredArguments = environment?.arguments
            ?.count { it is RequiredArgument } ?: 0

        val templateText = List(numberRequiredArguments) { "{\$__Variable${it}\$}" }.joinToString("") + "\n\$END\$"
        val parameterTemplate = object : TemplateImpl("", templateText, "") {
            override fun isToReformat(): Boolean = true
        }
        repeat(numberRequiredArguments) { parameterTemplate.addVariable(TextExpression(""), true) }

        TemplateManager.getInstance(context.project)
            .startTemplate(context.editor, parameterTemplate)
    }

    /**
     * Remove whitespaces and everything after that that was inserted by the lookup text.
     */
    private fun removeWhiteSpaces(context: InsertionContext) {
        val editor = context.editor
        val document = editor.document
        val offset = editor.caretModel.offset
        // context.startOffset is the offset of the start of the just inserted text.
        val insertedText = document.text.substring(context.startOffset, offset)
        val indexFirstSpace = insertedText.indexOfFirst { it == ' ' }
        if (indexFirstSpace == -1) return
        document.deleteString(context.startOffset + indexFirstSpace, offset)
    }

    /**
     * Makes sure environments get imported if required.
     *
     * @author Hannah Schellekens
     */
    private inner class EnvironmentInsertImports(val context: InsertionContext) : TemplateEditingListener {

        override fun beforeTemplateFinished(templateState: TemplateState, template: Template?) {
            val envName = templateState.getVariableValue("__Variable0")?.text ?: return

            val environment = Environment[envName]
            insertRequiredArguments(environment, context)
            environment ?: return

            val pack = environment.dependency
            val file = context.file
            val editor = context.editor
            val envDefinitions = file.definitionsAndRedefinitionsInFileSet().asSequence()
                .filter { it.isEnvironmentDefinition() }
                .mapNotNull { it.requiredParameter(0) }
                .toSet()

            // Include packages.
            if (!file.includedPackages().contains(pack) && envName !in envDefinitions) {
                file.insertUsepackage(pack)
            }

            // Add initial contents.
            val initial = environment.initialContents
            editor.insertAndMove(editor.caretModel.offset, initial)
        }

        override fun templateFinished(template: Template, b: Boolean) {}
        override fun templateCancelled(template: Template?) {}
        override fun currentVariableChanged(templateState: TemplateState, template: Template?, i: Int, i1: Int) {}
        override fun waitingForInput(template: Template?) {}
    }
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import nl.hannahsten.texifyidea.file.BibtexFile
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.files.bibtexIdsInFileSet
import nl.hannahsten.texifyidea.util.files.referencedFileSet

/**
 * @author Sten Wessel
 */
class LatexReferenceInsertHandler(private val remote: Boolean = false, private val remoteBib: BibtexEntry? = null) : InsertHandler<LookupElement> {

    override fun handleInsert(context: InsertionContext, item: LookupElement) {
        val model = context.editor.caretModel
        model.moveToOffset(model.offset + 1)

        if (remote) {
            remoteBib ?: return
            val bibsInFile = context.file.originalFile.bibtexIdsInFileSet()
            // Add the bib item after the last item we found in the file set, and hope that that makes sense...
            bibsInFile.lastOrNull()?.let {
                it.parent.addAfter(remoteBib, it)
            }

            // If there are no bib items in the fileset yet, see if there is a(n empty) bib file we can add the bib entry to.
            if (bibsInFile.isEmpty()) {
                context.file.originalFile
                    .referencedFileSet()
                    .firstOrNull { it is BibtexFile }
                    ?.add(remoteBib)
            }
        }
    }
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * Makes the caret skip to the end of the parent command.
 *
 * @author Hannah Schellekens
 */
object MoveToEndOfCommandHandler : InsertHandler<LookupElement> {

    override fun handleInsert(context: InsertionContext, item: LookupElement) {
        val editor = context.editor
        val caret = editor.caretModel
        val file = context.file
        val element = file.findElementAt(caret.offset) ?: return
        val command = element.parentOfType(LatexCommands::class) ?: return
        caret.moveToOffset(command.endOffset())
    }
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.editor.Editor
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.util.magic.TypographyMagic

/**
 * Inserts the right part of left-right command pairs, like `\left( \right)`.
 *
 * @author Hannah Schellekens
 */
open class RightInsertHandler : InsertHandler<LookupElement> {

    override fun handleInsert(context: InsertionContext, element: LookupElement) {
        val editor = context.editor
        val command = element.`object` as? LatexCommand ?: return
        val name = command.command

        when {
            name.startsWith("left") -> insertRightBraceCommand(name, editor)
            name == "langle" -> insertRightCommand(editor, "\\rangle", spacing = " ")
            name == "lq" -> insertRightCommand(editor, "\\rq", spacing = "", suffix = "{}")
        }
    }

    private fun insertRightBraceCommand(commandName: String, editor: Editor) {
        val char = commandName.substring(4)
        val opposite = TypographyMagic.braceOpposites[char] ?: return
        editor.document.insertString(editor.caretModel.offset, "  \\right$opposite")
        editor.caretModel.moveToOffset(editor.caretModel.offset + 1)
    }

    private fun insertRightCommand(editor: Editor, rightCommand: String, spacing: String = "", suffix: String = "") {
        editor.document.insertString(editor.caretModel.offset, "$suffix$spacing$spacing$rightCommand$suffix")
        editor.caretModel.moveToOffset(editor.caretModel.offset + spacing.length + suffix.length)
    }
}

package nl.hannahsten.texifyidea.completion.handlers

import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.template.TemplateManager
import nl.hannahsten.texifyidea.lang.BibtexEntryType

/**
 * @author Hannah Schellekens
 */
object TokenTypeInsertHandler : InsertHandler<LookupElement> {

    override fun handleInsert(context: InsertionContext, item: LookupElement) {
        val inserted = item.`object` as? BibtexEntryType ?: return
        when (inserted.token) {
            "string" -> insertString(context)
            "preamble" -> insertPreamble(context)
            else -> insertType(inserted, context)
        }
    }

    private fun insertString(context: InsertionContext) {
        val editor = context.editor
        val document = editor.document
        val caret = editor.caretModel
        val offset = caret.offset

        document.insertString(offset, "{  }")
        caret.moveToOffset(offset + 2)
    }

    private fun insertPreamble(context: InsertionContext) {
        val editor = context.editor
        val document = editor.document
        val caret = editor.caretModel
        val offset = caret.offset

        document.insertString(offset, "{\n    \"\"\n}")
        caret.moveToOffset(offset + 7)
    }

    private fun insertType(inserted: BibtexEntryType, context: InsertionContext) {
        val template = inserted.template()
        val editor = context.editor
        val templateManager = TemplateManager.getInstance(context.project)
        templateManager.startTemplate(editor, template)
    }
}

package nl.hannahsten.texifyidea.completion.pathcompletion

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.util.files.getParentDirectoryByImportPaths

/**
 * @author Hannah Schellekens
 */
class LatexFileProvider : LatexPathProviderBase() {

    override fun selectScanRoots(file: PsiFile): List<VirtualFile> {
        val searchDirs = getProjectRoots().toMutableList()
        val allIncludeCommands = LatexIncludesIndex.getItems(file)
        for (command in allIncludeCommands) {
            searchDirs.addAll(getParentDirectoryByImportPaths(command))
        }
        return searchDirs
    }

    override fun searchFolders(): Boolean = true

    override fun searchFiles(): Boolean = true
}

package nl.hannahsten.texifyidea.completion.pathcompletion

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.util.files.findRelativeSearchPathsForImportCommands

/**
 * Provide base folder for autocompleting folders.
 */
class LatexFolderProvider : LatexPathProviderBase() {

    override fun selectScanRoots(file: PsiFile): List<VirtualFile> {
        val searchDirs = getProjectRoots().toMutableList()
        val allIncludeCommands = LatexIncludesIndex.getItems(file)
        for (command in allIncludeCommands) {
            searchDirs.addAll(findRelativeSearchPathsForImportCommands(command))
        }
        return searchDirs
    }

    override fun searchFolders(): Boolean = true

    override fun searchFiles(): Boolean = false
}

package nl.hannahsten.texifyidea.completion.pathcompletion

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.files.*
import nl.hannahsten.texifyidea.util.magic.cmd
import java.io.File

/**
 * Autocompletion roots based on graphicspaths.
 */
class LatexGraphicsPathProvider : LatexPathProviderBase() {

    override fun selectScanRoots(file: PsiFile): ArrayList<VirtualFile> {
        val paths = getProjectRoots()

        val rootDirectory = file.findRootFile().containingDirectory.virtualFile
        getGraphicsPathsInFileSet(file).forEach {
            paths.add(rootDirectory.findVirtualFileByAbsoluteOrRelativePath(it) ?: return@forEach)
        }

        return paths
    }

    /**
     * When using \includegraphics from graphicx package, a path prefix can be set with \graphicspath.
     * @return Graphicspaths defined in the fileset.
     */
    fun getGraphicsPathsInFileSet(file: PsiFile): List<String> {
        val graphicsPaths = mutableListOf<String>()
        val graphicsPathCommands = file.commandsInFileSet().filter { it.name == LatexGenericRegularCommand.GRAPHICSPATH.cmd }

        // Is a graphicspath defined?
        if (graphicsPathCommands.isNotEmpty()) {
            // Only last defined one counts
            graphicsPathCommands.last().getGraphicsPaths().forEach { graphicsPaths.add(it) }
        }

        return graphicsPaths
    }

    /**
     * This function is used in [InputFileReference#resolve], which is also used to create the file set, so we should
     * not use the file set here (to avoid an infinite loop). Instead, we start searching the file of the given command
     * for graphics paths. Then look at all commands that include the file of the given command and check the files of
     * those commands for graphics paths.
     */
    fun getGraphicsPathsWithoutFileSet(command: LatexCommands): List<String> {
        fun graphicsPathsInFile(file: PsiFile): List<String> = file.commandsInFile()
            .filter { it.name == "\\graphicspath" }
            .flatMap { it.getGraphicsPaths() }

        // First find all graphicspaths commands in the file of the given command
        val graphicsPaths = graphicsPathsInFile(command.containingFile).toMutableList()

        val allIncludeCommands = LatexIncludesIndex.getItems(command.project)
        // Commands which may include the current file (this is an overestimation, better would be to check for RequiredFileArguments)
        var includingCommands = allIncludeCommands.filter { includeCommand -> includeCommand.getRequiredParameters().any { it.contains(command.containingFile.name.removeFileExtension()) } }

        // Avoid endless loop (in case of a file inclusion loop)
        val maxDepth = allIncludeCommands.size
        var counter = 0

        // I think it's a kind of reversed BFS
        while (includingCommands.isNotEmpty() && counter < maxDepth) {
            val handledFiles = mutableListOf<PsiFile>()
            val newIncludingCommands = mutableListOf<LatexCommands>()

            for (includingCommand in includingCommands) {
                // Search the file of the command that includes the current file for graphics paths.
                graphicsPaths.addAll(graphicsPathsInFile(includingCommand.containingFile))

                // Find files/commands to search next
                val file = includingCommand.containingFile
                if (file !in handledFiles) {
                    val commandsIncludingThisFile = allIncludeCommands.filter { includeCommand -> includeCommand.getRequiredParameters().any { it.contains(file.name) } }
                    newIncludingCommands.addAll(commandsIncludingThisFile)
                    handledFiles.add(file)
                }
            }

            includingCommands = newIncludingCommands
            counter++
        }

        return graphicsPaths
    }

    /**
     * Get all the graphics paths defined by one \graphicspaths command.
     */
    private fun LatexCommands.getGraphicsPaths(): List<String> {
        if (name != "\\graphicspath") return emptyList()
        return parameterList.firstNotNullOfOrNull { it.requiredParam }
            // Each graphics path is in a group.
            ?.childrenOfType(LatexNormalText::class)
            ?.map { it.text }
            // Relative paths (not starting with /) have to be appended to the directory of the file of the given command.
            ?.map { if (it.startsWith('/')) it else containingFile.containingDirectory.virtualFile.path + File.separator + it }
            ?: emptyList()
    }

    override fun searchFolders(): Boolean = true

    override fun searchFiles(): Boolean = true
}

package nl.hannahsten.texifyidea.completion.pathcompletion

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.util.parentOfTypes
import com.intellij.util.PlatformIcons
import com.intellij.util.ProcessingContext
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.completion.handlers.CompositeHandler
import nl.hannahsten.texifyidea.completion.handlers.FileNameInsertionHandler
import nl.hannahsten.texifyidea.completion.handlers.LatexReferenceInsertHandler
import nl.hannahsten.texifyidea.lang.commands.RequiredFileArgument
import nl.hannahsten.texifyidea.psi.LatexRequiredParam
import nl.hannahsten.texifyidea.util.expandCommandsOnce
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.isLatexFile
import java.io.File
import java.util.regex.Pattern

/**
 * @author Lukas Heiligenbrunner
 */
abstract class LatexPathProviderBase : CompletionProvider<CompletionParameters>() {

    private var parameters: CompletionParameters? = null
    private var resultSet: CompletionResultSet? = null
    private var validExtensions: Set<String>? = null
    private var absolutePathSupport = true

    companion object {

        private val TRIM_SLASH = Pattern.compile("/[^/]*$")
    }

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        this.parameters = parameters

        // We create a result set with the correct autocomplete text as prefix, which may be different when multiple LaTeX parameters (comma separated) are present
        val autocompleteText = processAutocompleteText(parameters.position.parentOfTypes(LatexRequiredParam::class)?.text ?: parameters.position.text)

        val parentCommand = context.get("type")
        if (parentCommand is RequiredFileArgument) {
            validExtensions = parentCommand.supportedExtensions
            absolutePathSupport = parentCommand.isAbsolutePathSupported
        }

        var finalCompleteText = expandCommandsOnce(autocompleteText, project = parameters.originalFile.project, file = parameters.originalFile) ?: autocompleteText

        // Process the expanded text again
        finalCompleteText = processAutocompleteText(finalCompleteText)
        resultSet = result.withPrefixMatcher(finalCompleteText)
        selectScanRoots(parameters.originalFile).forEach {
            addByDirectory(it, finalCompleteText)
        }
    }

    /**
     * return a List of Paths to be searched in
     * eg. project root
     * eg. \includegraphics roots
     */
    abstract fun selectScanRoots(file: PsiFile): Collection<VirtualFile>

    /**
     * enable folder search?
     */
    abstract fun searchFolders(): Boolean

    /**
     * enable file search?
     */
    abstract fun searchFiles(): Boolean

    /**
     * scan directory for completions
     */
    private fun addByDirectory(baseDirectory: VirtualFile, autoCompleteText: String) {
        // Check if path is relative or absolute
        if (File(autoCompleteText).isAbsolute) {
            if (absolutePathSupport) {
                // Split text in path and completion text
                val pathOffset = trimAutocompleteText(autoCompleteText)
                addAbsolutePathCompletion(pathOffset)
            }
        }
        else {
            val pathOffset = trimAutocompleteText(autoCompleteText)
            addRelativePathCompletion(baseDirectory, pathOffset)
        }
    }

    /**
     * add completion entries for relative path
     */
    private fun addRelativePathCompletion(projectDir: VirtualFile, pathOffset: String) {
        // Don't use LocalFileSystem.findByPath here. In a normal IntelliJ, projectDir.path will be the
        // full path (on the local file system) to the project directory, but in tests this will be just "src/"
        // causing LocalFileSystem.findByPath to always return null.
        projectDir.findFileByRelativePath(pathOffset)?.let { baseDir ->
            if (searchFolders()) {
                addFolderNavigations(pathOffset)
                getContents(baseDir, true).forEach {
                    addDirectoryCompletion(pathOffset, it)
                }
            }

            if (searchFiles()) getContents(baseDir, false).forEach {
                addFileCompletion(pathOffset, it)
            }
        }
    }

    /**
     * add completion entries for absolute path
     */
    private fun addAbsolutePathCompletion(baseDir: String) {
        LocalFileSystem.getInstance().findFileByPath(baseDir)?.let { dirFile ->
            if (searchFolders()) {
                addFolderNavigations(baseDir)
                getContents(dirFile, true).forEach {
                    addDirectoryCompletion(baseDir, it)
                }
            }

            if (searchFiles()) getContents(dirFile, false).forEach {
                addFileCompletion(baseDir, it)
            }
        }
    }

    /**
     * add basic folder navigation options such as ../ and ./
     */
    private fun addFolderNavigations(baseDir: String) {
        // Add current directory.
        resultSet?.addElement(
            LookupElementBuilder.create("$baseDir./")
                .withPresentableText(".")
                .withIcon(PlatformIcons.PACKAGE_ICON)
        )

        // Add return directory.
        resultSet?.addElement(
            LookupElementBuilder.create("$baseDir../")
                .withPresentableText("..")
                .withIcon(PlatformIcons.PACKAGE_ICON)
        )
    }

    /**
     * add Directory to autocompletion dialog
     */
    private fun addDirectoryCompletion(baseDir: String, foundFile: VirtualFile) {
        resultSet?.addElement(
            LookupElementBuilder.create(baseDir + foundFile.name + "/")
                .withPresentableText(foundFile.presentableName)
                .withIcon(PlatformIcons.PACKAGE_ICON)
        )
    }

    /**
     * add file to autocompletion dialog
     */
    private fun addFileCompletion(baseDir: String, foundFile: VirtualFile) {
        if (validExtensions != null) {
            if (validExtensions!!.contains(foundFile.extension).not()) return
        }

        val icon = TexifyIcons.getIconFromExtension(foundFile.extension)
        resultSet?.addElement(
            LookupElementBuilder.create(baseDir + foundFile.name)
                .withPresentableText(foundFile.presentableName)
                .withInsertHandler(
                    CompositeHandler(
                        LatexReferenceInsertHandler(),
                        FileNameInsertionHandler()
                    )
                )
                .withIcon(icon)
        )
    }

    /**
     * Get project roots
     * @return all Project Root directories as VirtualFile
     */
    fun getProjectRoots(): ArrayList<VirtualFile> {
        val resultList = ArrayList<VirtualFile>()

        parameters?.apply {
            // Get base data.
            val baseFile = this.originalFile.virtualFile

            if (this.originalFile.isLatexFile()) {
                this.originalFile.findRootFile().containingDirectory?.virtualFile?.apply {
                    resultList.add(this)
                }
            }
            else resultList.add(baseFile.parent)

            val rootManager = ProjectRootManager.getInstance(this.originalFile.project)
            rootManager.contentSourceRoots.asSequence()
                .filter { it != resultList.firstOrNull() }
                .toSet()
                .filterNotNull()
                .forEach { resultList.add(it) }
        }

        return resultList
    }

    /**
     * @param autoCompleteText full path (relative or absolute) including the completion offset
     * @return path without autocompletion text including forward slash at end
     */
    private fun trimAutocompleteText(autoCompleteText: String): String {
        return if (!autoCompleteText.contains("/")) {
            ""
        }
        else TRIM_SLASH.matcher(autoCompleteText).replaceAll("/")
        // delete last subpath occurence
    }

    /**
     * Prepare auto-complete text before searching for files.
     *
     * This does the following:
     * - Removes any start '{' and ending '}'
     * - Remove 'IntelliJIdeaRulezz'
     * - Removes any arguments before the last one (separated by ',')
     * - Remove starting './'
     * - Prevent '//' (removes the first '/')
     */
    private fun processAutocompleteText(autocompleteText: String): String {
        var result = autocompleteText.dropWhile { it == '{' }.dropLastWhile { it == '}' }.trim()

        // When the last parameter is autocompleted, parameters before that may also be present in
        // autocompleteText so we split on commas and take the last one. If it is not the last
        // parameter, no commas will be present so the split will do nothing.
        result = result.replace("IntellijIdeaRulezzz", "")
            .split(",").last()

        // Prevent double ./
        if (result.startsWith("./")) {
            result = result.substring(2)
        }
        // Prevent double ../
        if (result.startsWith("../")) {
            result = result.substring(3)
        }

        // Prevent double /
        if (result.startsWith("//")) {
            result = result.substring(1)
        }

        return result
    }

    /**
     * search in given path for subfiles or directories
     */
    private fun getContents(base: VirtualFile?, directory: Boolean): List<VirtualFile> {
        val contents = java.util.ArrayList<VirtualFile>()

        if (base == null) {
            return contents
        }

        for (file in base.children) {
            if (file.isDirectory == directory) {
                contents.add(file)
            }
        }

        return contents
    }
}

package nl.hannahsten.texifyidea.editor.autocompile

import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.openapi.util.Key
import nl.hannahsten.texifyidea.settings.TexifySettings

/**
 * When autocompile is enabled, update autocompile state when compilation is done.
 */
class AutoCompileDoneListener : ProcessListener {

    override fun processTerminated(event: ProcessEvent) {
        if (TexifySettings.getInstance().autoCompile) {
            AutoCompileState.compileDone()
        }
    }

    override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
    }

    override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) {
    }

    override fun startNotified(event: ProcessEvent) {
    }
}

package nl.hannahsten.texifyidea.editor.autocompile

import com.intellij.codeInsight.editorActions.BackspaceHandlerDelegate
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.files.isLatexFile

/**
 * On every added (or deleted, see [AutoCompileBackspacehandler]) character, make sure the document is compiled.
 */
class AutocompileHandler : TypedHandlerDelegate() {

    override fun charTyped(char: Char, project: Project, editor: Editor, file: PsiFile): Result {
        run {
            // Only do this for latex files and if the option is enabled
            if (file.fileType != LatexFileType || !TexifySettings.getInstance().autoCompile || TexifySettings.getInstance().autoCompileOnSaveOnly) {
                return@run
            }

            AutoCompileState.documentChanged(project)
        }

        return super.charTyped(char, project, editor, file)
    }
}

class AutoCompileBackspacehandler : BackspaceHandlerDelegate() {

    override fun beforeCharDeleted(c: Char, file: PsiFile, editor: Editor) {
        val project = editor.project
        if (file.isLatexFile() && project != null && TexifySettings.getInstance().autoCompile && !TexifySettings.getInstance().autoCompileOnSaveOnly) {
            AutoCompileState.documentChanged(project)
        }
    }

    override fun charDeleted(c: Char, file: PsiFile, editor: Editor): Boolean {
        return false
    }
}

package nl.hannahsten.texifyidea.editor.autocompile

import com.intellij.execution.ExecutionManager
import com.intellij.execution.ExecutionTargetManager
import com.intellij.execution.RunManager
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration

/**
 * State of autocompilation.
 *
 * Both the run configuration and the typedhandler should be able to access the same state, so we can run the correct run config for the correct file.
 */
object AutoCompileState {

    /** Whether an autocompile is in progress. */
    private var isCompiling = false

    /** Whether the document has changed since the last triggered autocompile. */
    private var hasChanged = false

    /** Needed to get the selected run config. */
    private var project: Project? = null

    /**
     * Tell the state the document has been changed by the user.
     */
    @Synchronized
    fun documentChanged(project: Project) {
        this.project = project

        // Remember that the document changed, so a compilation should be scheduled later
        if (isCompiling) {
            hasChanged = true
        }
        else {
            scheduleCompilation()
        }
    }

    /**
     * Tell the state a compilation has just finished.
     */
    @Synchronized
    fun compileDone() {
        // Only compile again if needed
        if (hasChanged) {
            scheduleCompilation()
        }
        else {
            isCompiling = false
        }
    }

    private fun scheduleCompilation() {
        if (project == null) {
            Notification("LaTeX", "Could not auto-compile", "Please make sure you have compiled the document first.", NotificationType.WARNING).notify(null)

            return
        }

        isCompiling = true
        hasChanged = false

        // Get run configuration selected in the combobox and run that one
        if (project!!.isDisposed) return
        val runConfigSettings = RunManager.getInstance(project!!).selectedConfiguration

        if (runConfigSettings?.configuration !is LatexRunConfiguration) {
            Notification("LaTeX", "Could not auto-compile", "Please make sure you have a valid LaTeX run configuration selected.", NotificationType.WARNING).notify(null)
            return
        }

        // Changing focus would interrupt the user during typing
        (runConfigSettings.configuration as LatexRunConfiguration).allowFocusChange = false

        ExecutionManager.getInstance(project!!).restartRunProfile(
            project!!,
            DefaultRunExecutor.getRunExecutorInstance(),
            ExecutionTargetManager.getInstance(project!!).activeTarget,
            runConfigSettings,
            null
        )
    }
}

package nl.hannahsten.texifyidea.editor.autocompile

import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.vfs.AsyncFileListener
import com.intellij.openapi.vfs.AsyncFileListener.ChangeApplier
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.openapi.wm.WindowManager
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.settings.TexifySettings

/**
 * If a LaTeX file is saved, do automatic compilation if desired.
 *
 * @author Thomas
 */
class AutoCompileVfsListener : AsyncFileListener {

    override fun prepareChange(events: MutableList<out VFileEvent>): ChangeApplier? {
        if (!TexifySettings.getInstance().autoCompileOnSaveOnly || !events.any { it.file?.fileType == LatexFileType }) return null
        return object : ChangeApplier {
            override fun afterVfsChange() {
                super.afterVfsChange()
                // In order to figure out which run configuration should run, we currently just run the selected run configuration in the currently open project
                // suggestParentWindow needs to run in EDT
                runInEdt {
                    val project = ProjectManager.getInstance().openProjects.firstOrNull {
                        WindowManager.getInstance().suggestParentWindow(it)?.isActive == true
                    } ?: return@runInEdt
                    AutoCompileState.documentChanged(project)
                }
            }
        }
    }
}

package nl.hannahsten.texifyidea.editor.autocompile

import com.intellij.codeInsight.editorActions.BackspaceHandlerDelegate
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.action.preview.ShowEquationPreview
import nl.hannahsten.texifyidea.action.preview.ShowTikzPreview
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.parser.findOuterMathEnvironment

/**
 * On every added (or deleted, see [ContinuousPreviewBackspacehandler]) character, initiate a preview.
 */
class ContinuousPreviewHandler : TypedHandlerDelegate() {

    override fun charTyped(char: Char, project: Project, editor: Editor, file: PsiFile): Result {
        run {
            // Only do this for latex files and if the option is enabled
            if (file.fileType != LatexFileType || !TexifySettings.getInstance().continuousPreview) {
                return@run
            }

            val element = file.findElementAt(editor.caretModel.offset) ?: return@run

            val textEditor = if (editor is TextEditor) {
                editor
            }
            else {
                ShowEquationPreview().getTextEditor(project, null) ?: return@run
            }

            // Show corresponding preview depending on environment
            if (element.findOuterMathEnvironment() != null) {
                ShowEquationPreview().actionPerformed(file.virtualFile, project, textEditor)
            }
            else if (ShowTikzPreview().findTikzEnvironment(element) != null) {
                ShowTikzPreview().actionPerformed(file.virtualFile, project, textEditor)
            }
        }

        return super.charTyped(char, project, editor, file)
    }
}

class ContinuousPreviewBackspacehandler : BackspaceHandlerDelegate() {

    override fun beforeCharDeleted(c: Char, file: PsiFile, editor: Editor) {}

    override fun charDeleted(c: Char, file: PsiFile, editor: Editor): Boolean {
        return if (file.isLatexFile()) {
            ContinuousPreviewHandler().charTyped(c, file.project, editor, file)
            true
        }
        else {
            // Returning true would block functionality of other plugins (e.g. removing second brace of pair in [<cursor>])
            false
        }
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexTypes
import nl.hannahsten.texifyidea.util.parser.childrenOfType

/**
 * Adds folding regions for BibTeX entries.
 *
 * @author Thomas Schouten
 */
class BibtexEntryFoldingBuilder : FoldingBuilderEx(), DumbAware {

    override fun isCollapsedByDefault(node: ASTNode) = false

    override fun getPlaceholderText(node: ASTNode): String {
        val type = node.findChildByType(BibtexTypes.TYPE)?.text
        val id = node.findChildByType(BibtexTypes.ID)?.text
        return if (type == null || id == null) {
            "..."
        }
        else {
            "$type{$id ...}"
        }
    }

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = ArrayList<FoldingDescriptor>()
        val entries = root.childrenOfType(BibtexEntry::class)

        for (entry in entries) {
            val start = entry.textOffset
            val end = entry.endtry.textOffset + 1 // 1 for the last bracket

            if (end <= start) {
                continue
            }

            descriptors.add(FoldingDescriptor(entry, TextRange(start, end)))
        }

        return descriptors.toTypedArray()
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.application.options.editor.CodeFoldingOptionsProvider
import com.intellij.openapi.options.BeanConfigurable

/**
 * Settings > Editor > General > Code Folding settings for LaTeX.
 */
class LatexCodeFoldingOptionsProvider : CodeFoldingOptionsProvider, BeanConfigurable<LatexCodeFoldingSettings>(LatexCodeFoldingSettings.getInstance(), "LaTeX") {

    init {
        val settings = instance
        if (settings != null) {
            checkBox("Package imports", settings::collapseImports)
        }
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.util.xmlb.XmlSerializerUtil

/**
 * Settings for [LatexCodeFoldingOptionsProvider], inspired by RsCodeFoldingSettings from the Rust plugin.
 * Other languages appear to mostly use editor.xml, so we use it as well.
 * Note: in the debug instance, this file is at build/idea-sandbox/config/options/editor.xml.
 */
@State(name = "LatexCodeFoldingSettings", storages = [Storage("editor.xml")])
class LatexCodeFoldingSettings : PersistentStateComponent<LatexCodeFoldingSettings> {

    var collapseImports: Boolean = true

    override fun getState() = this

    override fun loadState(state: LatexCodeFoldingSettings) = XmlSerializerUtil.copyBean(state, this)

    companion object {

        fun getInstance(): LatexCodeFoldingSettings = service()
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.util.parser.childrenOfType

/**
 * Enables folding of multiple comments on successive lines.
 *
 * @author jojo2357
 */
class LatexCommentFoldingBuilder : FoldingBuilderEx(), DumbAware {

    override fun isCollapsedByDefault(node: ASTNode) = false

    override fun getPlaceholderText(node: ASTNode): String {
        val parsedText = node.text.trim()
        return if (parsedText.length > 9) parsedText.substring(0, 8) + "..."
        else parsedText
    }

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = ArrayList<FoldingDescriptor>()
        val comments = root.childrenOfType(PsiComment::class)
        val whitespaces = root.childrenOfType(PsiWhiteSpace::class)

        val whitespaceLocations = ArrayList(whitespaces.map { TextRange(it.startOffset, it.endOffset) })

        // meld friends
        for (i in whitespaceLocations.size - 1 downTo 1) {
            if (whitespaceLocations[i].startOffset == whitespaceLocations[i - 1].endOffset) {
                whitespaceLocations[i - 1] =
                    TextRange(whitespaceLocations[i - 1].startOffset, whitespaceLocations[i].endOffset)
                whitespaceLocations.removeAt(i)
            }
        }

        var collectedTextRange: TextRange? = null
        var parentCollapse: PsiElement? = null

        for (comment in comments) {
            // Initialization: start with the first comment in a possible sequence
            if (collectedTextRange == null) {
                collectedTextRange = TextRange(comment.startOffset, comment.endOffset)
                parentCollapse = comment.originalElement
            }
            else {
                // If the next comment follows directly after the previous one (and was broken by whitespace) add the next comment to the sequence
                if (whitespaceLocations.any { it.startOffset == collectedTextRange!!.endOffset && it.endOffset == comment.startOffset }) {
                    collectedTextRange = TextRange(collectedTextRange.startOffset, comment.endOffset)
                }
                else {
                    // Otherwise, stop the sequence and fold what we have so far
                    parentCollapse?.let {
                        descriptors.add(
                            buildDescriptor(parentCollapse, collectedTextRange)
                        )
                    }
                    collectedTextRange = TextRange(comment.startOffset, comment.endOffset)
                    parentCollapse = comment.originalElement
                }
            }
        }

        // Fold what we were building in the event we run out of comments (aka trailing whitespace in file)
        if (parentCollapse != null && collectedTextRange != null) {
            if (collectedTextRange.endOffset > collectedTextRange.startOffset)
                parentCollapse.let {
                    descriptors.add(buildDescriptor(parentCollapse, collectedTextRange))
                }
            parentCollapse = null
            collectedTextRange = null
        }

        return descriptors.toTypedArray()
    }

    private fun buildDescriptor(
        parentCollapse: PsiElement?,
        collectedTextRange: TextRange?
    ) = FoldingDescriptor(
        parentCollapse!!,
        TextRange(collectedTextRange!!.startOffset, collectedTextRange.endOffset)
    )
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.LatexMagicComment
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.endOffset

/**
 * Implements custom folding regions.
 * See https://blog.jetbrains.com/idea/2012/03/custom-code-folding-regions-in-intellij-idea-111/
 *
 * Syntax:
 * %!<editor-fold desc="MyDescription">
 * %!</editor-fold>
 *
 * %!region MyDescription
 * %!endregion
 *
 * @author Thomas
 */
class LatexCustomFoldingBuilder : FoldingBuilderEx(), DumbAware {

    private val startRegionRegex = """%!\s*(<editor-fold desc="|region ?)(?<description>.*?)(">)?\s*$""".toRegex()
    private val endRegionRegex = """%!\s*(</editor-fold>|endregion)""".toRegex()

    override fun isCollapsedByDefault(node: ASTNode) = false

    override fun getPlaceholderText(node: ASTNode) = "..."

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = ArrayList<FoldingDescriptor>()
        val magicComments = root.childrenOfType(LatexMagicComment::class)
        val startRegions = magicComments.filter { startRegionRegex.containsMatchIn(it.text) }
            .sortedBy { it.textOffset }.toMutableList()
        val endRegions = magicComments.filter { endRegionRegex.containsMatchIn(it.text) }
            .sortedBy { it.textOffset }

        // Now we need to match ends with starts
        for (end in endRegions) {
            // Match with the closest available start: it does not make sense to match that start with anything else, because ranges can be nested but not partially overlap
            val start = startRegions.lastOrNull { it.textOffset < end.textOffset } ?: continue
            val description = startRegionRegex.find(start.text)?.groups?.get("description")?.value?.ifBlank { "..." } ?: "..."
            startRegions.remove(start)
            descriptors.add(FoldingDescriptor(start.node, TextRange(start.textOffset, end.endOffset()), null, description))
        }

        return descriptors.toTypedArray()
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.endOffset

/**
 * Adds folding regions for LaTeX environments.
 *
 * Enables folding of `\begin{environment} ... \end{environment}`.
 *
 * @author Sten Wessel
 */
class LatexEnvironmentFoldingBuilder : FoldingBuilderEx(), DumbAware {

    override fun isCollapsedByDefault(node: ASTNode) = false

    override fun getPlaceholderText(node: ASTNode) = "..."

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = ArrayList<FoldingDescriptor>()
        val environments = root.childrenOfType(LatexEnvironment::class)

        for (environment in environments) {
            // Get content offsets.
            // Uses the commands instead of the actual contents as they may be empty.
            val start = environment.beginCommand.endOffset()
            val end = environment.endCommand?.textOffset ?: continue

            if (end <= start) {
                continue
            }

            descriptors.add(FoldingDescriptor(environment, TextRange(start, end)))
        }

        return descriptors.toTypedArray()
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.FoldingGroup
import com.intellij.openapi.project.DumbAware
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.util.files.commandsInFile

/**
 * @author Johannes Berger
 */
class LatexEscapedSymbolFoldingBuilder : FoldingBuilderEx(), DumbAware {

    override fun isCollapsedByDefault(node: ASTNode) = true

    override fun getPlaceholderText(node: ASTNode): String? = null

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val group = FoldingGroup.newGroup("EscapedSymbolFoldingGroup")
        val file = root.containingFile

        return file.commandsInFile()
            .filter { it.commandToken.text in commandsToFold }
            .map { FoldingDescriptor(it.node, it.textRange, group, it.commandToken.text.substringAfter("\\")) }
            .toTypedArray()
    }

    companion object {

        val commandsToFold = setOf("%", "#", "&", "_", "$").map { "\\" + it }
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexRequiredParam
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.magic.CommandMagic.foldableFootnotes

/**
 * Adds folding regions for LaTeX environments.
 *
 * Enables folding of `\footnote{}`.
 *
 * @author jojo2357
 */
class LatexFootnoteFoldingBuilder : FoldingBuilderEx(), DumbAware {

    override fun isCollapsedByDefault(node: ASTNode) = true

    override fun getPlaceholderText(node: ASTNode): String {
        val parsedText = node.text.substring(1).trim()
        return if (parsedText.length > 8) parsedText.substring(0, 8) + "..." else parsedText.substring(0, parsedText.length - 1)
    }

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = ArrayList<FoldingDescriptor>()
        val parameters =
            root.childrenOfType(LatexCommands::class).filter { foldableFootnotes.contains(it.name) }.mapNotNull {
                it.firstChildOfType(LatexRequiredParam::class)
            }

        for (environment in parameters) {
            if (environment.endOffset - 1 > environment.startOffset + 1)
                descriptors.add(FoldingDescriptor(environment.originalElement, TextRange(environment.startOffset + 1, environment.endOffset - 1)))
        }

        return descriptors.toTypedArray()
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexNoMathContent
import nl.hannahsten.texifyidea.util.parser.allCommands
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.parser.parentOfType

/**
 * Folds multiple \\usepackage or \\RequirePackage statements.
 * This is not DumbAware.
 *
 * @author Hannah Schellekens
 */
open class LatexImportFoldingBuilder : FoldingBuilderEx() {

    companion object {

        private val includesSet = setOf(LatexGenericRegularCommand.USEPACKAGE.cmd, LatexGenericRegularCommand.REQUIREPACKAGE.cmd)
    }

    override fun isCollapsedByDefault(node: ASTNode) = LatexCodeFoldingSettings.getInstance().collapseImports

    override fun getPlaceholderText(node: ASTNode) = if (node.text.contains("RequirePackage")) "\\RequirePackage{...}" else "\\usepackage{...}"

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = ArrayList<FoldingDescriptor>()
        val covered = HashSet<LatexCommands>()
        val commands = root.allCommands().filter { it.name in includesSet }

        for (command in commands) {
            // Do not cover commands twice.
            if (command in covered) {
                continue
            }

            // Iterate over all consecutive commands.
            var next: LatexCommands? = command
            var last: LatexCommands = command
            while (next != null && next.name in includesSet) {
                covered += next
                last = next
                next = next.nextCommand()
            }

            descriptors.add(FoldingDescriptor(command, TextRange(command.startOffset, last.endOffset)))
        }

        return descriptors.toTypedArray()
    }

    private fun PsiElement.nextCommand(): LatexCommands? {
        val content = if (this is LatexCommands) {
            parentOfType(LatexNoMathContent::class) ?: return null
        }
        else this
        val next = content.nextSibling

        // When having multiple breaks, don't find new commands to fold.
        // When whitespace without multiple breaks, look further.
        if (next is PsiWhiteSpace) {
            return if (PatternMagic.containsMultipleNewlines.matcher(next.text).matches()) {
                null
            }
            else next.nextCommand()
        }

        // No next? Then there is no valid next command. This happens e.g. when it's the last element of the file.
        next ?: return null

        // Skip comments.
        if (next is PsiComment) {
            return next.nextCommand()
        }

        return next.firstChildOfType(LatexCommands::class)
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.FoldingGroup
import com.intellij.openapi.project.DumbAware
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.lang.commands.LatexMathCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexMathEnvironment
import nl.hannahsten.texifyidea.util.parser.childrenOfType

/**
 * @author Sten Wessel
 */
class LatexMathSymbolFoldingBuilder : FoldingBuilderEx(), DumbAware {

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = listOf<FoldingDescriptor>().toMutableList()
        val mathEnvironments = root.childrenOfType(LatexMathEnvironment::class)

        for (mathEnvironment in mathEnvironments) {
            val group = FoldingGroup.newGroup("latexMathSymbol")
            val commands = mathEnvironment.childrenOfType(LatexCommands::class)

            for (command in commands) {
                val display = LatexMathCommand[command.commandToken.text.substring(1)]?.first()?.display ?: continue

                descriptors.add(object : FoldingDescriptor(command.commandToken.node, command.commandToken.textRange, group) {
                    override fun getPlaceholderText() = display
                })
            }
        }

        return descriptors.toTypedArray()
    }

    override fun isCollapsedByDefault(node: ASTNode) = true

    override fun getPlaceholderText(node: ASTNode): String? = null
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.lang.magic.DefaultMagicKeys
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.nextSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.previousSiblingIgnoreWhitespace

/**
 * Recursively folds section commands
 *
 * @author Tom Evers
 */
open class LatexSectionFoldingBuilder : FoldingBuilderEx() {

    companion object {

        private val sectionCommandNames = CommandMagic.sectioningCommands.map { it.command }
        private val sectionCommands = sectionCommandNames.map { "\\$it" }.toTypedArray()
    }

    override fun isCollapsedByDefault(node: ASTNode) = false

    override fun getPlaceholderText(node: ASTNode) = node.text + "..."

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = ArrayList<FoldingDescriptor>()
        val commands = root.childrenOfType<LatexCommands>().filter { it.name in sectionCommands }
            .sortedBy { it.textOffset }
        val comments = root.childrenOfType<LatexMagicComment>().filter { it.key() == DefaultMagicKeys.FAKE }
        val sectionElements: List<PsiElement> = (commands + comments).sortedBy { it.textOffset }

        if (sectionElements.isEmpty()) {
            return descriptors.toTypedArray()
        }

        /**
         * Get the name/section value from a [LatexCommands] or a [LatexMagicComment].
         */
        fun PsiElement.name() = when (this) {
            is LatexCommands -> name?.dropWhile { it == '\\' }
            // Take the first word of the value because the fake section can have a 'Title',
            // e.g. %! fake subsection Introduction.
            is LatexMagicComment -> value()?.trim()?.split(" ")?.firstOrNull()
            else -> null
        }

        for (currentFoldingCommandIndex in sectionElements.indices) {
            var foundHigherCommand = false
            val currentFoldingCommand = sectionElements[currentFoldingCommandIndex]
            val currentCommandRank = sectionCommandNames.indexOf(currentFoldingCommand.name() ?: continue)

            // Find the 'deepest' section under currentFoldingCommand,
            // so when we find something that is equal or lower in rank
            // (a section is ranked lower than a subsection) then we
            // get the block of text between it and the currentFoldingCommand
            for (nextFoldingCommandIndex in currentFoldingCommandIndex + 1 until sectionElements.size) {
                val nextFoldingCommand = sectionElements[nextFoldingCommandIndex]

                // Find the rank of the next command to compare with the current rank
                val nextCommandRank = sectionCommandNames.indexOf(nextFoldingCommand.name())

                // If we found a command which is ranked lower, save the block of text inbetween
                // Note that a section is ranked lower than a subsection
                if (nextCommandRank <= currentCommandRank) {
                    // Get the location of the next folding command
                    val end = nextFoldingCommand.parentOfType(LatexNoMathContent::class)?.previousSiblingIgnoreWhitespace()
                        ?: break

                    // Get the text range between the current and the next folding command
                    if (end.textOffset + end.textLength - currentFoldingCommand.textOffset > 0) {
                        val foldingRange = TextRange(currentFoldingCommand.textOffset, end.textOffset + end.textLength)

                        // Add it as a folding block
                        descriptors.add(FoldingDescriptor(currentFoldingCommand, foldingRange))
                    }

                    foundHigherCommand = true
                    break
                }
            }

            /*
             * If this item is the topmost level of the section structure or the last section marker,
             * use the end of all text as the end of the range.
             */
            if (!foundHigherCommand) {
                val foldingContent = sectionElements.last().parentOfType(LatexNoMathContent::class) ?: continue
                var nextContent: LatexNoMathContent? = foldingContent
                var previousContent: LatexNoMathContent = foldingContent
                while (nextContent != null) {
                    previousContent = nextContent
                    nextContent = nextContent.nextSiblingIgnoreWhitespace() as? LatexNoMathContent
                }
                if (previousContent.textOffset + previousContent.textLength - currentFoldingCommand.textOffset > 0) {
                    val foldingRange = TextRange(
                        currentFoldingCommand.textOffset,
                        previousContent.textOffset + previousContent.textLength
                    )
                    descriptors.add(FoldingDescriptor(currentFoldingCommand, foldingRange))
                }
            }
        }
        return descriptors.toTypedArray()
    }
}

package nl.hannahsten.texifyidea.editor.folding

import com.intellij.lang.ASTNode
import com.intellij.lang.folding.FoldingBuilderEx
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.FoldingGroup
import com.intellij.openapi.project.DumbAware
import com.intellij.psi.PsiElement
import com.intellij.psi.tree.TokenSet
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.psi.LatexTypes.NORMAL_TEXT_WORD
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.shiftRight
import nl.hannahsten.texifyidea.util.toTextRange

/**
 * Folding symbols that are not escaped, like en dashes.
 * Similar to [LatexEscapedSymbolFoldingBuilder].
 */
class LatexSymbolFoldingBuilder : FoldingBuilderEx(), DumbAware {

    override fun isCollapsedByDefault(node: ASTNode) = true

    override fun getPlaceholderText(node: ASTNode): String? = null

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val group = FoldingGroup.newGroup("SymbolFoldingGroup")
        val file = root.containingFile

        // Fold all hyphens in the document
        return file.childrenOfType<LatexNormalText>().flatMap { it.node.getChildren(TokenSet.create(NORMAL_TEXT_WORD)).toSet() }
            .filter { "--" in it.text }
            .flatMap { node ->
                val text = node.text
                "-{2,}".toRegex().findAll(text).map {
                    val range = it.range.shiftRight(node.startOffset).toTextRange()
                    FoldingDescriptor(node, range, group, getPlaceholderText(it.value))
                }
            }
            .toTypedArray()
    }

    private fun getPlaceholderText(string: String): String {
        return when (string) {
            // The following are covered by the DASH token type
            "-" -> "-" // hyphen
            "--" -> "–" // en dash
            "---" -> "—" // em dash
            else -> string
        }
    }
}

package nl.hannahsten.texifyidea.editor.postfix

import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplate
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateProvider
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile

class LatexPostFixTemplateProvider : PostfixTemplateProvider, CompletionContributor() {

    private val templates = mutableSetOf<PostfixTemplate>(
        LatexWrapWithGroupPostfixTemplate,
        LatexWrapWithOpenGroupPostfixTemplate,
        LatexWrapWithInlineMathPostfixTemplate
    )

    private val wrapWithTextCommandTemplates = mutableSetOf<PostfixTemplate>(
        LatexWrapWithBoldFacePostfixTemplate,
        LatexWrapWithItalicFacePostfixTemplate,
        LatexWrapWithEmphPostfixTemplate,
        LatexWrapWithTypewriterPostfixTemplate
    )

    private val wrapWithMathCommandTemplates = mutableSetOf<PostfixTemplate>(
        LatexWrapWithTildePostfixTemplate,
        LatexWrapWithHatPostfixTemplate,
        LatexWrapWithBarPostfixTemplate,
        LatexWrapWithSquareRootPostfixTemplate,
        LatexWrapWithOverlinePostfixTemplate,
        LatexWrapWithUnderlinePostfixTemplate,
        LatexWrapWithMathcalPostfixTemplate
    )

    override fun getTemplates(): MutableSet<PostfixTemplate> = (templates + wrapWithTextCommandTemplates + wrapWithMathCommandTemplates) as MutableSet<PostfixTemplate>

    override fun isTerminalSymbol(currentChar: Char): Boolean = (currentChar == '.')

    override fun afterExpand(file: PsiFile, editor: Editor) {}

    override fun preCheck(copyFile: PsiFile, realEditor: Editor, currentOffset: Int): PsiFile =
        copyFile

    override fun preExpand(file: PsiFile, editor: Editor) {}
}

package nl.hannahsten.texifyidea.editor.postfix

import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateExpressionSelector
import com.intellij.openapi.editor.Document
import com.intellij.psi.PsiElement
import com.intellij.psi.util.elementType
import com.intellij.util.Function
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.inMathContext

class LatexPostfixExpressionSelector(private val mathOnly: Boolean = false, private val textOnly: Boolean = false) : PostfixTemplateExpressionSelector {

    override fun hasExpression(context: PsiElement, copyDocument: Document, newOffset: Int): Boolean {
        return when {
            mathOnly -> context.inMathContext()
            textOnly -> !context.inMathContext()
            else -> true
        }
    }

    override fun getRenderer(): Function<PsiElement, String> {
        return Function(PsiElement::getText)
    }

    override fun getExpressions(context: PsiElement, document: Document, offset: Int): MutableList<PsiElement> {
        return when (context.elementType) {
            LatexTypes.INLINE_MATH_END -> mutableListOf(context.firstParentOfType(LatexInlineMath::class) as PsiElement)
            LatexTypes.CLOSE_BRACE ->
                mutableListOf(
                    context.firstParentOfType(LatexCommands::class)
                        ?: context.firstParentOfType(LatexGroup::class) as PsiElement
                )
            LatexTypes.CLOSE_BRACKET -> mutableListOf(context.firstParentOfType(LatexOptionalParam::class) as PsiElement)
            LatexTypes.DISPLAY_MATH_END -> mutableListOf(context.firstParentOfType(LatexDisplayMath::class) as PsiElement)
            else -> mutableListOf(context)
        }
    }
}

package nl.hannahsten.texifyidea.editor.postfix

import com.intellij.codeInsight.template.postfix.templates.PostfixTemplate
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateProvider
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.insertUsepackage

abstract class LatexPostfixTemplateFromPackageProvider(private val pack: LatexPackage) : PostfixTemplateProvider {

    abstract override fun getTemplates(): MutableSet<PostfixTemplate>

    override fun isTerminalSymbol(currentChar: Char): Boolean = (currentChar == '.' || currentChar == ',')

    override fun afterExpand(file: PsiFile, editor: Editor) {
        file.insertUsepackage(pack)
    }

    override fun preCheck(copyFile: PsiFile, realEditor: Editor, currentOffset: Int): PsiFile =
        copyFile

    override fun preExpand(file: PsiFile, editor: Editor) {}

    companion object {

        fun getProvider(pack: LatexPackage?): PostfixTemplateProvider {
            return when (pack) {
                LatexPackage.AMSMATH -> LatexPostfixTemplateFromAmsMathProvider()
                LatexPackage.AMSFONTS -> LatexPostfixTemplateFromAmsFontsProvider()
                LatexPackage.BM -> LatexPostfixTemplateFromBmProvider()
                else -> LatexPostFixTemplateProvider()
            }
        }
    }
}

class LatexPostfixTemplateFromAmsMathProvider : LatexPostfixTemplateFromPackageProvider(LatexPackage.AMSMATH) {

    override fun getTemplates(): MutableSet<PostfixTemplate> = mutableSetOf(
        LatexWrapWithTextPostfixTemplate
    )
}

class LatexPostfixTemplateFromAmsFontsProvider : LatexPostfixTemplateFromPackageProvider(LatexPackage.AMSFONTS) {

    override fun getTemplates(): MutableSet<PostfixTemplate> = mutableSetOf(
        LatexWrapWithMathbbPostfixTemplate
    )
}

class LatexPostfixTemplateFromBmProvider : LatexPostfixTemplateFromPackageProvider(LatexPackage.BM) {

    override fun getTemplates(): MutableSet<PostfixTemplate> = mutableSetOf(
        LatexWrapWithBmPostfixTemplate
    )
}

package nl.hannahsten.texifyidea.editor.postfix

import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateProvider
import com.intellij.codeInsight.template.postfix.templates.StringBasedPostfixTemplate
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.lang.LatexPackage

/**
 * When adding a template that inserts a command that requires a package, make
 * sure that there exists a [LatexPostfixTemplateFromPackageProvider] for this
 * package, and add it to [LatexPostfixTemplateFromPackageProvider.getProvider].
 * Don't forget to add this provider to plugin.xml.
 */

/* General wrappers. */
internal object LatexWrapWithInlineMathPostfixTemplate : ConstantStringBasedPostfixTemplate(
    "math",
    "\$expr\$",
    "$$\$expr$$$\$END$",
    textOnly = true
)

internal object LatexWrapWithGroupPostfixTemplate : ConstantStringBasedPostfixTemplate(
    "braces",
    "{expr}",
    "{\$expr$}\$END$"
)

internal object LatexWrapWithOpenGroupPostfixTemplate : ConstantStringBasedPostfixTemplate(
    "brackets",
    "[expr]",
    "[\$expr$]\$END$"
)

internal object LatexWrapWithTextPostfixTemplate : ConstantStringBasedPostfixTemplate(
    "text",
    "\\text{expr}",
    "\\text{\$expr$\$more$}\$END$",
    mathOnly = true,
    pack = LatexPackage.AMSMATH
) {

    override fun setVariables(template: Template, element: PsiElement) {
        template.addVariable("more", "", "", true)
    }
}

/* Wrap with text command postfix template. */
internal object LatexWrapWithBoldFacePostfixTemplate : LatexWrapWithCommandPostfixTemplate("textbf", name = "bf", textOnly = true)
internal object LatexWrapWithItalicFacePostfixTemplate : LatexWrapWithCommandPostfixTemplate("textit", name = "it", textOnly = true)
internal object LatexWrapWithEmphPostfixTemplate : LatexWrapWithCommandPostfixTemplate("emph", textOnly = true)
internal object LatexWrapWithTypewriterPostfixTemplate : LatexWrapWithCommandPostfixTemplate("texttt", name = "tt", textOnly = true)

/* Wrap with math command postfix templates. */
internal object LatexWrapWithBarPostfixTemplate : LatexWrapWithCommandPostfixTemplate("bar", mathOnly = true)
internal object LatexWrapWithHatPostfixTemplate : LatexWrapWithCommandPostfixTemplate("hat", mathOnly = true)
internal object LatexWrapWithTildePostfixTemplate : LatexWrapWithCommandPostfixTemplate("tilde", mathOnly = true)
internal object LatexWrapWithSquareRootPostfixTemplate : LatexWrapWithCommandPostfixTemplate("sqrt", mathOnly = true)
internal object LatexWrapWithOverlinePostfixTemplate : LatexWrapWithCommandPostfixTemplate("overline", mathOnly = true)
internal object LatexWrapWithUnderlinePostfixTemplate : LatexWrapWithCommandPostfixTemplate("underline", mathOnly = true)
internal object LatexWrapWithMathbbPostfixTemplate : LatexWrapWithCommandPostfixTemplate("mathbb", name = "bb", mathOnly = true, pack = LatexPackage.AMSFONTS)
internal object LatexWrapWithBmPostfixTemplate : LatexWrapWithCommandPostfixTemplate("bm", mathOnly = true, pack = LatexPackage.BM)
internal object LatexWrapWithMathcalPostfixTemplate : LatexWrapWithCommandPostfixTemplate("mathcal", name = "cal", mathOnly = true)

internal open class LatexWrapWithCommandPostfixTemplate(commandName: String, name: String = commandName, mathOnly: Boolean = false, textOnly: Boolean = false, pack: LatexPackage? = null) : ConstantStringBasedPostfixTemplate(
    name,
    "\\$commandName{expr}",
    "\\$commandName{\$expr$}\$END$",
    mathOnly, textOnly, pack
)

internal abstract class ConstantStringBasedPostfixTemplate(
    name: String,
    desc: String,
    private val template: String,
    mathOnly: Boolean = false,
    textOnly: Boolean = false,
    pack: LatexPackage? = null,
    provider: PostfixTemplateProvider = LatexPostfixTemplateFromPackageProvider.getProvider(pack)
) : StringBasedPostfixTemplate(name, desc, LatexPostfixExpressionSelector(mathOnly, textOnly), provider) {

    override fun getTemplateString(element: PsiElement) = template

    override fun getElementToRemove(expr: PsiElement?) = expr
}

package nl.hannahsten.texifyidea.editor.surroundwith

class CustomFoldingRegionSurrounder : LatexSurrounder("%! region\n", "\n%! endregion", displayBefore = "region", displayAfter = "endregion")

package nl.hannahsten.texifyidea.editor.surroundwith

import com.intellij.lang.surroundWith.SurroundDescriptor
import com.intellij.lang.surroundWith.Surrounder
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile

class LatexSurroundDescriptor : SurroundDescriptor {

    /**
     * Get the first and last element of the selection, as these are the only
     * elements the [LatexSurrounder] uses to surround a piece of text.
     */
    override fun getElementsToSurround(file: PsiFile?, startOffset: Int, endOffset: Int): Array<PsiElement> {
        val startElement = file?.findElementAt(startOffset) ?: return emptyArray()
        val endElement = file.findElementAt(endOffset - 1) ?: return emptyArray()
        return arrayOf(startElement, endElement)
    }

    override fun isExclusive(): Boolean = false

    override fun getSurrounders(): Array<Surrounder> = arrayOf(
        DoubleQuotesSurrounder(),
        SingleQuotesSurrounder(),
        CustomFoldingRegionSurrounder(),
    )
}

package nl.hannahsten.texifyidea.editor.surroundwith

import com.intellij.lang.surroundWith.Surrounder
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.parser.endOffset

/**
 * Surrounds selected text with the given strings.
 *
 * @param displayBefore 'Before' string to display in popup.
 */
open class LatexSurrounder(private val before: String, private val after: String, val displayBefore: String = before, val displayAfter: String = after) : Surrounder {

    override fun isApplicable(elements: Array<out PsiElement>): Boolean {
        return true
    }

    /**
     * Puts [before] before the first element of [elements], and puts [after]
     * after the last element of [elements].
     *
     * [elements] contains only the first and last psi element in the to be surrounded range.
     *
     * @return range to select/to position the caret
     */
    override fun surroundElements(project: Project, editor: Editor, elements: Array<out PsiElement>): TextRange? {
        LatexPsiHelper(project).createFromText(before).children
            .map { it.node }
            .forEach { elements.first().parent.node.addChild(it, elements.first().node) }
        LatexPsiHelper(project).createFromText(after).children
            .map { it.node }.reversed() // Insert before the (changing) nextSibling
            .forEach { elements.last().parent.node.addChild(it, elements.last().nextSibling?.node) }

        val endOffset = elements.last().endOffset() + after.length
        return TextRange(endOffset, endOffset)
    }

    override fun getTemplateDescription(): String {
        return "Surround with $displayBefore...$displayAfter"
    }
}

open class LatexPairSurrounder(surroundPair: Pair<String, String>) : LatexSurrounder(surroundPair.first, surroundPair.second)

package nl.hannahsten.texifyidea.editor.surroundwith

import nl.hannahsten.texifyidea.util.getOpenAndCloseQuotes

/**
 * Surrounder to surround a selection with double opening and closing quotes, dependent
 * on the user setting.
 *
 * @author Abby Berkers
 */
open class QuotesSurrounder(val char: Char) : LatexPairSurrounder(getOpenAndCloseQuotes(char))

class DoubleQuotesSurrounder : QuotesSurrounder('"')

class SingleQuotesSurrounder : QuotesSurrounder('\'')

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.BibtexFileType

/**
 * This class provides the auto-insertion of a second double quote when one double quote is typed in bibtex files.
 *
 * @author Hannah Schellekens
 */
open class BibtexQuoteInsertHandler : TypedHandlerDelegate() {

    override fun charTyped(char: Char, project: Project, editor: Editor, file: PsiFile): Result {
        if (file.fileType != BibtexFileType) {
            return super.charTyped(char, project, editor, file)
        }

        val document = editor.document
        val caret = editor.caretModel
        val offset = caret.offset

        if (char != '"' || document.textLength < offset + 1) {
            return super.charTyped(char, project, editor, file)
        }

        if (offset < 0 || offset + 1 >= document.textLength) {
            return super.charTyped(char, project, editor, file)
        }

        // Do not insert a quote when there is one right in front of the cursor
        if (document.getText(TextRange.from(offset, 1)) == "\"") {
            document.deleteString(offset, offset + 1)
            return Result.STOP
        }

        editor.document.insertString(editor.caretModel.offset, "\"")
        return super.charTyped(char, project, editor, file)
    }
}

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.codeInsight.editorActions.BackspaceHandlerDelegate
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase.getElementAtCaret
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.util.caretOffset
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.get
import nl.hannahsten.texifyidea.util.parser.inVerbatim
import nl.hannahsten.texifyidea.util.orFalse

/**
 * In the situation $<caret>$, pressing backspace should delete both $.
 * See [LatexTypedHandler] for how the second $ was inserted.
 *
 * @author Thomas
 */
class InlineMathBackspaceHandler : BackspaceHandlerDelegate() {

    override fun beforeCharDeleted(c: Char, file: PsiFile, editor: Editor) {
        if (c == '$' && file.isLatexFile() && !getElementAtCaret(editor)?.inVerbatim().orFalse()) {
            val offset = editor.caretOffset()
            if (file.document()?.get(offset) == LatexGenericRegularCommand.DOLLAR_SIGN.command) {
                file.document()?.deleteString(offset, offset + 1)
            }
        }
    }

    override fun charDeleted(c: Char, file: PsiFile, editor: Editor): Boolean {
        return false
    }
}

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.util.IncorrectOperationException
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.psi.LatexTypes

/**
 * In the situation \[<caret>\], when pressing enter the caret
 * should end up with the correct indent.
 *
 * @author Sten Wessel
 */
class LatexEnterBetweenBracesHandler : EnterHandlerDelegateAdapter() {

    override fun preprocessEnter(file: PsiFile, editor: Editor, caretOffset: Ref<Int>, caretAdvance: Ref<Int>, dataContext: DataContext, originalHandler: EditorActionHandler?): EnterHandlerDelegate.Result {
        if (file !is LatexFile) {
            return EnterHandlerDelegate.Result.Continue
        }

        val offset = caretOffset.get()
        val before = file.findElementAt(offset - 1)
        val after = file.findElementAt(offset + 1)

        if (before == null || after == null) {
            return EnterHandlerDelegate.Result.Continue
        }

        if (before.node.elementType === LatexTypes.DISPLAY_MATH_START && after.node.elementType === LatexTypes.DISPLAY_MATH_END) {
            originalHandler!!.execute(editor, editor.caretModel.currentCaret, dataContext)
            PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.document)
            try {
                CodeStyleManager.getInstance(file.getProject()).adjustLineIndent(file, editor.caretModel.offset)
            }
            catch (e: IncorrectOperationException) {
                Logger.getInstance(javaClass).error(e)
            }

            return EnterHandlerDelegate.Result.DefaultForceIndent
        }

        return EnterHandlerDelegate.Result.Continue
    }
}

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.application.options.CodeStyle
import com.intellij.formatting.Block
import com.intellij.formatting.ChildAttributes
import com.intellij.formatting.Indent
import com.intellij.lang.ASTNode
import nl.hannahsten.texifyidea.formatting.LatexBlock
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.settings.codestyle.LatexCodeStyleSettings
import nl.hannahsten.texifyidea.util.parser.firstChildOfType

/**
 * Configure the indent after pressing enter.
 *
 * Technically, this is not a typed handler, but it's easier to find this way.
 */
object LatexEnterHandler {

    fun getChildAttributes(newChildIndex: Int, node: ASTNode, subBlocks: MutableList<Block>): ChildAttributes {
        val shouldIndentDocumentEnvironment =
            CodeStyle.getCustomSettings(node.psi.containingFile, LatexCodeStyleSettings::class.java).INDENT_DOCUMENT_ENVIRONMENT
        val shouldIndentEnvironment = node.elementType === LatexTypes.ENVIRONMENT && (
            (node.psi as? LatexEnvironment)
                ?.firstChildOfType(LatexBeginCommand::class)
                ?.firstChildOfType(LatexParameterText::class)?.text != "document" || shouldIndentDocumentEnvironment
            )

        val type = node.elementType
        if (type === LatexTypes.DISPLAY_MATH || shouldIndentEnvironment) {
            return ChildAttributes(Indent.getNormalIndent(true), null)
        }
        val indentSections = CodeStyle.getCustomSettings(node.psi.containingFile, LatexCodeStyleSettings::class.java).INDENT_SECTIONS
        if (indentSections) {
            // This function will be called on the block for which the caret is adding something in the children at the given index,
            // however this may mean that the current block may be Content and a block will be added in the last child of the children of this block (so not directly into the children of this block).
            // Therefore to find the section indent of the line the caret was on, we need the previous leaf in the tree
            var currentBlock = subBlocks.getOrNull(newChildIndex - 1)
            var indentSize = (currentBlock as? LatexBlock)?.sectionIndent ?: 0
            while (currentBlock != null && !currentBlock.isLeaf) {
                currentBlock = currentBlock.subBlocks.lastOrNull()
                if (currentBlock is LatexBlock) {
                    indentSize = Integer.max(indentSize, currentBlock.sectionIndent)
                }
            }

            if (indentSize > 0) {
                // We may need to provide more than one indent, because of the problem that the parent-child relationship
                // does not match the indent sizes
                val singleIndentSize = CodeStyle.getIndentSize(node.psi.containingFile)
                return ChildAttributes(Indent.getSpaceIndent(indentSize * singleIndentSize), null)
            }
        }
        return ChildAttributes(Indent.getNoneIndent(), null)
    }
}

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiInvalidElementAccessException
import com.intellij.psi.util.PsiUtilCore
import nl.hannahsten.texifyidea.psi.LatexMagicComment
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.get

class LatexEnterInCommentHandler : EnterHandlerDelegateAdapter() {

    // Used to enable the preprocess to tell the postprocess what to do
    private var isComment = false

    override fun preprocessEnter(
        file: PsiFile,
        editor: Editor,
        caretOffset: Ref<Int>,
        caretAdvance: Ref<Int>,
        dataContext: DataContext,
        originalHandler: EditorActionHandler?
    ): EnterHandlerDelegate.Result {
        val element = file.findElementAt(caretOffset.get())
        isComment = element is PsiComment || element?.parent is LatexMagicComment
        return super.preprocessEnter(file, editor, caretOffset, caretAdvance, dataContext, originalHandler)
    }

    override fun postProcessEnter(file: PsiFile, editor: Editor, dataContext: DataContext): EnterHandlerDelegate.Result {
        if (isComment) {
            handleEnterInComment(file, editor)
        }
        return super.postProcessEnter(file, editor, dataContext)
    }

    /**
     * Insert extra % sign when enter was pressed in a comment.
     */
    private fun handleEnterInComment(file: PsiFile, editor: Editor) {
        // Check location of the % on the previous line, especially the number of spaces before and after it
        // because that's what we want to have on the current line as well
        // (note the cursor is on the second line, after the enter has been processed)
        val currentCaret = editor.caretModel.currentCaret

        if (!currentCaret.isValid) return
        try {
            PsiUtilCore.ensureValid(file)
        }
        catch (e: PsiInvalidElementAccessException) {
            return
        }

        val previousLineNumber = (currentCaret.logicalPosition.line - 1).coerceAtLeast(0)
        val lineStart = editor.document.getLineStartOffset(previousLineNumber)
        val lineEnd = editor.document.getLineEndOffset(previousLineNumber)
        val previousLine = editor.document[IntRange(lineStart, lineEnd)]

        // If we pressed enter right before or after the %, then don't do anything, as we're not actually splitting a comment
        // To clarify the second case: consider wanting to type a % at the end of a line
        if (!previousLine.contains("%") || previousLine.endsWith("%")) {
            return
        }

        // Since the enter handler already inserted the indent of the previous line, subtract it
        val numberOfSpacesBeforePercent = previousLine.takeWhile { it == ' ' }.length
        val numberOfSpacesAfterPercent = previousLine.dropWhile { it == ' ' }
            // ! for magic comments
            .dropWhile { it == '%' || it == '!' }
            .takeWhile { it == ' ' }.length

        // Include magic comments
        val commentPrefix = """^\s*(?<percent>[%!]+)""".toRegex().find(previousLine)?.groups?.get("percent")?.value ?: return

        if (numberOfSpacesBeforePercent == 0) {
            // It was right at the start of the line, so we only have to insert it there again
            // and insert the spaces that were probably not part of the indent (which is already done by the enter handler)
            val startOfLine = editor.caretModel.currentCaret.visualLineStart
            file.document()?.insertString(startOfLine, commentPrefix + " ".repeat(numberOfSpacesAfterPercent % 4))
        }
        else {
            // There was indent before it, but that's handled by the enter handler already
            // So we just need to insert any spaces after it
            file.document()?.insertString(editor.caretModel.currentCaret.offset, commentPrefix + " ".repeat(numberOfSpacesAfterPercent))
        }
        // Alternatively, consider the setting CodeStyle.getLanguageSettings(file).LINE_COMMENT_AT_FIRST_COLUMN

        PsiDocumentManager.getInstance(file.project).commitDocument(editor.document)
    }
}

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.codeInsight.editorActions.AutoHardWrapHandler
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate.Result
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TextExpression
import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import com.intellij.openapi.editor.actions.SplitLineAction.SPLIT_LINE_KEY
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.*

/**
 * @author Hannah Schellekens
 */
class LatexEnterInEnumerationHandler : EnterHandlerDelegate {

    override fun postProcessEnter(
        file: PsiFile, editor: Editor,
        context: DataContext
    ): Result {
        if (file.fileType != LatexFileType) {
            return Result.Continue
        }

        // Don't insert \item when the enter was triggered by the word wrap
        if (DataManager.getInstance().loadFromDataContext(context, AutoHardWrapHandler.AUTO_WRAP_LINE_IN_PROGRESS_KEY) == true) {
            return Result.Continue
        }

        val caret = editor.caretModel
        val element = file.findElementAt(caret.offset)
        if (hasValidContext(element, context)) {
            val previousMarker = getPreviousMarker(element!!)
            if (previousMarker == null) {
                editor.insertAtCaretAndMove("\\item ")
            }
            else {
                // Use live template, so that the user can choose to replace the label and press enter to jump out of the optional argument
                val template = TemplateImpl("", "\\item[\$__Variable0\$] ", "")
                template.addVariable(TextExpression(previousMarker.trim('[', ']')), true)
                TemplateManager.getInstance(file.project).startTemplate(editor, template)
            }
        }

        return Result.Continue
    }

    override fun preprocessEnter(
        file: PsiFile, editor: Editor, p2: Ref<Int>, p3: Ref<Int>,
        context: DataContext,
        p5: EditorActionHandler?
    ): Result {
        return Result.Continue
    }

    /**
     * Get the special marker that is used at the previous item (if any).
     */
    private fun getPreviousMarker(element: PsiElement): String? {
        val environment = element.parentOfType(LatexEnvironment::class) ?: return null

        // Last element in the list => Find last \item.
        val label = if (element.parent is LatexEnvironment) {
            getLastLabel(environment)
        }
        // Middle of the list => Find previous \item.
        else {
            getPreviousLabel(element)
        } ?: return null // when no label could befound.

        // Extract optional parameters.
        val optionals = label.childrenOfType(LatexOptionalParam::class).firstOrNull() ?: return null
        return optionals.text
    }

    /**
     * Finds the last label in the environment.
     *
     * @return The last label in the environment, or `null` when there are no labels.
     */
    private fun getLastLabel(environment: PsiElement): LatexCommands? {
        // Only consider direct children, because there could be nested enumerations which we should ignore
        return environment.children
            .firstOrNull { it is LatexEnvironmentContent }
            ?.children
            ?.flatMap { it.children.toList() }
            ?.filterIsInstance<LatexCommands>()
            ?.lastOrNull { it.name == "\\item" }
    }

    /**
     * Finds the label that comes before `element`.
     *
     * @return The previous label, or `null` when it couldn't be found.
     */
    private fun getPreviousLabel(element: PsiElement): LatexCommands? {
        var sibling: PsiElement? = element.previousSiblingIgnoreWhitespace()

        while (sibling != null) {
            val label = getLastLabel(sibling)
            if (label != null) {
                return label
            }

            sibling = sibling.previousSiblingIgnoreWhitespace()
        }

        return null
    }

    /**
     * Checks if insertion of `\item` is desired.
     *
     * @return `true` insertion desired, `false` insertion not desired or element is `null`.
     */
    private fun hasValidContext(element: PsiElement?, context: DataContext): Boolean {
        if (
            !TexifySettings.getInstance().automaticItemInItemize ||
            element == null ||
            DataManager.getInstance().loadFromDataContext(context, SPLIT_LINE_KEY) == true || // SplitLineAction means Ctrl+Enter was pressed, we decide to not insert \item in that case
            element.inMathContext()
        ) {
            return false
        }

        val isGluedToTheBeginCommand = element.hasParent(LatexBeginCommand::class)
        val isInsideAnEnumeration = element.inDirectEnvironment(EnvironmentMagic.listingEnvironments)
        return isInsideAnEnumeration && !isGluedToTheBeginCommand
    }
}

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
import com.intellij.openapi.editor.CaretModel
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.getOpenAndCloseQuotes
import nl.hannahsten.texifyidea.util.parser.inVerbatim
import nl.hannahsten.texifyidea.util.insertUsepackage
import kotlin.math.min

/**
 * This class performs smart quote substitution. When this is enabled, it will replace double quotes " and single quotes ' with the appropriate LaTeX symbols.
 *
 * @author Thomas Schouten
 */
open class LatexQuoteInsertHandler : TypedHandlerDelegate() {

    override fun charTyped(char: Char, project: Project, editor: Editor, file: PsiFile): Result {
        // Only do this for latex files and if the option is enabled
        if (file.fileType != LatexFileType || TexifySettings.getInstance().automaticQuoteReplacement == TexifySettings.QuoteReplacement.NONE) {
            return super.charTyped(char, project, editor, file)
        }

        val document = editor.document
        val caret = editor.caretModel
        val offset = caret.offset

        // Disable in verbatim context.
        val psi = file.findElementAt(offset)
        if (psi?.inVerbatim() == true) return super.charTyped(char, project, editor, file)

        // Only do smart things with double and single quotes
        if (char != '"' && char != '\'') {
            return super.charTyped(char, project, editor, file)
        }

        // Check if we are not out of the document range
        if (offset - 1 < 0 || offset - 1 >= document.textLength) {
            return super.charTyped(char, project, editor, file)
        }

        insertReplacement(document, file, caret, offset, char)

        return super.charTyped(char, project, editor, file)
    }

    /**
     * Insert either opening or closing quotes.
     *
     * This behaviour is inspired by the smart quotes functionality of TeXworks, source:
     * https://github.com/TeXworks/texworks/blob/2f902e2e429fad3e2bbb56dff07c823d1108adf4/src/CompletingEdit.cpp#L762
     */
    private fun insertReplacement(document: Document, file: PsiFile, caret: CaretModel, offset: Int, char: Char) {
        val replacementPair = getOpenAndCloseQuotes(char)
        val openingQuotes = replacementPair.first
        val closingQuotes = if (TexifySettings.getInstance().automaticQuoteReplacement == TexifySettings.QuoteReplacement.CSQUOTES) "" else replacementPair.second

        // The default replacement of the typed double quotes is a pair of closing quotes
        var isOpeningQuotes = false

        // Always use opening quotes at the beginning of the document
        if (offset == 1) {
            isOpeningQuotes = true
        }
        else {
            // Character before the cursor
            val previousChar = document.getText(TextRange.from(offset - 2, 1))

            if ((previousChar.firstOrNull())?.isLetter() == true && char == '\'') return

            // Don't replace when trying to type an escaped quote \"
            if (previousChar == "\\") return

            // Assume that if the previous char is a space, we are not closing anything
            if (previousChar == " ") {
                isOpeningQuotes = true
            }

            // After opening brackets, also use opening quotes
            if (previousChar == "{" || previousChar == "[" || previousChar == "(") {
                isOpeningQuotes = true
            }

            // If we are not closing the quotes, assume we are opening it (instead of doing nothing)
            if (TexifySettings.getInstance().automaticQuoteReplacement == TexifySettings.QuoteReplacement.CSQUOTES &&
                document.getText(TextRange.from(min(offset, document.textLength - 1), 1)) != "}"
            ) {
                isOpeningQuotes = true
            }
        }

        val replacement = if (isOpeningQuotes) openingQuotes else closingQuotes

        // Insert the replacement of the typed character, recall the offset is now right behind the inserted char
        document.deleteString(offset - 1, offset)
        document.insertString(offset - 1, replacement)

        // Move the cursor behind the replacement which replaced the typed char
        caret.moveToOffset(min(offset + replacement.length - 1, document.textLength))

        handleCsquotesInsertion(document, file, isOpeningQuotes, caret, char)
    }

    /**
     * Special behaviour for \enquote, because it is a command, not just opening and closing quotes.
     */
    private fun handleCsquotesInsertion(document: Document, file: PsiFile, isOpeningQuotes: Boolean, caret: CaretModel, char: Char) {
        if (TexifySettings.getInstance().automaticQuoteReplacement == TexifySettings.QuoteReplacement.CSQUOTES) {
            if (isOpeningQuotes) {
                // Insert } after the cursor to close the command
                document.insertString(caret.offset, "}")
            }
            else {
                // Instead of typing closing quotes, skip over the closing }
                caret.moveToOffset(caret.offset + 1)
            }

            // Package dependencies
            if (char == '"') {
                file.insertUsepackage(LatexGenericRegularCommand.ENQUOTE.dependency)
            }
            else {
                file.insertUsepackage(LatexGenericRegularCommand.ENQUOTE_STAR.dependency)
            }
        }
    }
}

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.codeInsight.CodeInsightSettings
import com.intellij.codeInsight.editorActions.TabOutScopesTracker
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import com.intellij.psi.tree.IElementType
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiUtilBase.getElementAtCaret
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.LatexInlineMath
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.settings.TexifySettings.Companion.getInstance
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.orFalse
import nl.hannahsten.texifyidea.util.parser.inVerbatim

/**
 * @author Sten Wessel
 */
class LatexTypedHandler : TypedHandlerDelegate() {

    /**
     * When pressing $ when text is selected, surround with $ signs.
     */
    override fun beforeSelectionRemoved(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
        val selectionModel = editor.selectionModel
        if (CodeInsightSettings.getInstance().SURROUND_SELECTION_ON_QUOTE_TYPED && selectionModel.hasSelection()) {
            if (file is LatexFile && c == '$') {
                val selectedText = selectionModel.selectedText
                val selectionStartOffset = selectionModel.selectionStart
                val selectionEndOffset = selectionModel.selectionEnd
                // Remove the selected text.
                editor.document.deleteString(selectionStartOffset, selectionEndOffset)
                // Insert the selected text, surrounded by dollar signs.
                editor.document.insertString(selectionStartOffset, "$c$selectedText$c")
                // Move the caret to just before the second dollar sign (same behaviour as surrounding with quotes).
                editor.caretModel.moveToOffset(selectionEndOffset + 1)
                return Result.STOP
            }
        }
        return super.beforeSelectionRemoved(c, project, editor, file)
    }

    override fun beforeCharTyped(c: Char, project: Project, editor: Editor, file: PsiFile, fileType: FileType): Result {
        if (file is LatexFile) {
            if (c == '$') {
                val caret = editor.caretModel
                val element = file.findElementAt(caret.offset)
                val parent = PsiTreeUtil.getParentOfType(element, LatexInlineMath::class.java) ?: return Result.CONTINUE
                val endOffset = parent.textRange.endOffset
                if (caret.offset == endOffset - 1 && parent.text.last() == c) {
                    // Caret is at the end of the environment, so run over the closing $
                    caret.moveCaretRelatively(1, 0, false, false, true)
                    return Result.STOP
                }
            }
        }
        return Result.CONTINUE
    }

    override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
        if (file is LatexFile && !getElementAtCaret(editor)?.inVerbatim().orFalse()) {
            val dollarSign = LatexGenericRegularCommand.DOLLAR_SIGN.command.toCharArray().first()
            if (c == dollarSign && getInstance().automaticSecondInlineMathSymbol) {
                val tokenType = getTypedTokenType(editor)
                if (tokenType !== LatexTypes.COMMAND_TOKEN && tokenType !== LatexTypes.COMMENT_TOKEN && tokenType !== LatexTypes.INLINE_MATH_END) {
                    editor.document.insertString(
                        editor.caretModel.offset, c.toString()
                    )
                    // Use CodeInsightSettings.getInstance().TAB_EXITS_BRACKETS_AND_QUOTES
                    TabOutScopesTracker.getInstance().registerEmptyScope(editor, editor.caretModel.offset)
                    return Result.STOP
                }
            }
            else if (c == '[') {
                return insertDisplayMathClose(editor)
            }
            else if (c == '(') {
                return insertRobustInlineMathClose(editor)
            }
            else if (c == '{') {
                return insertClosingEscapeBrace(editor)
            }
        }
        return Result.CONTINUE
    }

    /**
     * Disable autocomplete when typing a period, otherwise starting a new line
     * at the end of a line would be impossible (because postfix templates).
     */
    override fun checkAutoPopup(charTyped: Char, project: Project, editor: Editor, file: PsiFile): Result {
        return when {
            charTyped != '.' -> {
                super.checkAutoPopup(charTyped, project, editor, file)
            }
            file.isLatexFile() -> {
                Result.STOP
            }
            else -> {
                super.checkAutoPopup(charTyped, project, editor, file)
            }
        }
    }

    /**
     * Upon typing `\[`, inserts the closing delimiter `\]`.
     */
    private fun insertDisplayMathClose(editor: Editor): Result {
        val tokenType = getTypedTokenType(editor)
        if (tokenType === LatexTypes.DISPLAY_MATH_START) {
            // Checks if a bracket has already been inserted, if so: don't insert a 2nd one.
            val offset = editor.caretModel.offset
            if (offset >= editor.document.textLength) return Result.CONTINUE
            val bracketHuh = editor.document.getText(TextRange.from(offset, 1))
            val insertString = "\\" + if ("]" == bracketHuh) "" else "]"
            editor.document.insertString(offset, insertString)
            return Result.STOP
        }
        return Result.CONTINUE
    }

    /**
     * Upon typing `\(`, inserts the closing delimiter `\)`.
     */
    private fun insertRobustInlineMathClose(editor: Editor): Result {
        val tokenType = getTypedTokenType(editor)
        if (tokenType === LatexTypes.INLINE_MATH_START) {
            // Only insert backslash because the closing parenthesis is already inserted by the PairedBraceMatcher.
            editor.document.insertString(editor.caretModel.offset, "\\")
            return Result.STOP
        }
        return Result.CONTINUE
    }

    /**
     * Upon typing `\{`, inserts the closing delimiter `\}`. Unlike the others, this isnt a token so we just have to check manually
     */
    private fun insertClosingEscapeBrace(editor: Editor): Result {
        val offset = editor.caretModel.offset
        if (offset > editor.document.textLength) return Result.CONTINUE
        if (offset - 2 < 0) return Result.CONTINUE
        if (editor.document.getText(TextRange.from(offset - 2, 2)) == "\\{") {
            editor.document.insertString(editor.caretModel.offset, "\\}")
            return Result.STOP
        }
        return Result.CONTINUE
    }

    /**
     * Retrieves the token type of the character just typed.
     */
    private fun getTypedTokenType(editor: Editor): IElementType {
        val caret = editor.caretModel.offset
        val highlighter = (editor as EditorEx).highlighter
        val iterator = highlighter.createIterator(caret - 1)
        return iterator.tokenType
    }
}

package nl.hannahsten.texifyidea.editor.typedhandlers

import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.tree.LeafPsiElement
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.psi.LatexTypes.*
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.parser.*

/**
 * In the situation $a_3<caret>$, typing 'b' should end up in $a_{32}$ as that is probably what the user intended to type.
 *
 * @author Hannah Schellekens
 */
open class UpDownAutoBracket : TypedHandlerDelegate() {

    companion object {

        /**
         * Symbols that denote whether a {} block has to be inserted when having more than 1 character.
         */
        private val insertSymbols = setOf("_", "^")

        /**
         * Matches the suffix that denotes that braces may be inserted.
         */
        private val insertOnly =
            """^[a-zA-Z0-9]$""".toRegex()
    }

    override fun charTyped(c: Char, project: Project, editor: Editor, file: PsiFile): Result {
        if (!TexifySettings.getInstance().automaticUpDownBracket) {
            return Result.CONTINUE
        }

        if (file.fileType != LatexFileType) {
            return Result.CONTINUE
        }

        // Only insert when a valid symbol has been typed.
        val afterSymbol = c.toString()
        if (!insertOnly.matches(afterSymbol)) {
            return Result.CONTINUE
        }

        // Find selected element.
        val caret = editor.caretModel
        val element = file.findElementAt(caret.offset - 1) ?: return Result.CONTINUE

        // Check if in \label.
        val parent = element.parentOfType(LatexCommands::class)
        when (parent?.name) {
            "\\label", "\\bibitem" -> return Result.CONTINUE
        }

        // Insert squiggly brackets.
        if (element is LatexNormalText) {
            handleNormalText(element, editor)
        }
        else {
            val normalText = findNormalText(element)
            if (normalText != null) {
                handleNormalText(normalText, editor)
            }
        }

        return Result.CONTINUE
    }

    private fun findNormalText(element: PsiElement): LatexNormalText? {
        return when (element) {
            is PsiWhiteSpace -> {
                // When the whitespace is the end of a math environment.
                val sibling = element.previousSiblingIgnoreWhitespace() ?: return element.parentOfType(LatexNormalText::class)
                return when (sibling) {
                    is LatexMathContent, is LatexEnvironmentContent -> {
                        sibling.lastChildOfType(LatexNoMathContent::class)
                            ?.firstChildOfType(LatexNormalText::class)
                    }
                    is LeafPsiElement -> {
                        sibling.parentOfType(LatexNormalText::class)
                    }
                    else -> {
                        sibling.firstChildOfType(LatexNormalText::class)
                    }
                }
            }
            is PsiComment -> {
                // When for some reason people want to insert it directly before a comment.
                val mother = element.previousSiblingIgnoreWhitespace() ?: return null
                return mother.lastChildOfType(LatexNormalText::class)
            }
            is LeafPsiElement -> {
                when (element.elementType) {
                    END_COMMAND, BEGIN_COMMAND, COMMAND_TOKEN -> {
                        // When it is followed by a LatexCommands or comment tokens.
                        val noMathContent = element.firstParentOfType(LatexNoMathContent::class) ?: return null
                        val sibling = noMathContent.previousSiblingIgnoreWhitespace() ?: return null
                        return sibling.firstChildOfType(LatexNormalText::class)
                    }
                    INLINE_MATH_END -> {
                        // At the end of inline math.
                        val mathContent = element.previousSiblingIgnoreWhitespace() as? LatexMathContent ?: return null
                        val noMathContent = mathContent.lastChildOfType(LatexNoMathContent::class) ?: return null
                        return noMathContent.firstChildOfType(LatexNormalText::class)
                    }
                    else -> {
                        // When a character is inserted just before the close brace of a group/inline math end.
                        val content = element.prevSibling ?: return null
                        return content.firstChildOfType(LatexNormalText::class)
                    }
                }
            }
            else -> null
        }
    }

    private fun handleNormalText(normalText: LatexNormalText, editor: Editor) {
        // Check if in math environment.
        if (!normalText.inMathContext()) {
            return
        }

        val text = normalText.text
        val offset = normalText.textOffset
        val caret = editor.caretModel
        val relative = caret.offset - normalText.textOffset

        if (relative < 3 || text.length < relative - 1) {
            return
        }

        // Check if the inserted symbol is eligible for brace insertion.
        val subSupSymbol = text.substring(relative - 3, relative - 2)
        if (!insertSymbols.contains(subSupSymbol)) {
            return
        }

        insert(offset + relative - 3, editor)
    }

    private fun insert(symbolOffset: Int, editor: Editor) {
        val document = editor.document
        document.insertString(symbolOffset + 1, "{")
        document.insertString(symbolOffset + 4, "}")
    }
}

package nl.hannahsten.texifyidea.file.listeners

import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import nl.hannahsten.texifyidea.util.files.ReferencedFileSetService
import nl.hannahsten.texifyidea.util.magic.FileMagic

class VfsChangeListener : BulkFileListener {

    override fun after(events: MutableList<out VFileEvent>) {
        // Drop cache when files are added/deleted
        events.filter { event -> !event.isFromSave && event.file?.extension in FileMagic.fileTypes.map { it.defaultExtension } }
            .forEach {
                if (it.file != null) {
                    // We drop all caches because that is faster than figuring out which cached values need to be updated
                    ReferencedFileSetService.getInstance().dropAllCaches()
                }
            }
        super.after(events)
    }
}

package nl.hannahsten.texifyidea.formatting.spacingrules

import com.intellij.formatting.ASTBlock
import com.intellij.formatting.Spacing
import com.intellij.openapi.project.ProjectManager
import com.intellij.psi.codeStyle.CommonCodeStyleSettings
import nl.hannahsten.texifyidea.formatting.createSpacing
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexEnvironmentContent
import nl.hannahsten.texifyidea.psi.getEnvironmentName
import nl.hannahsten.texifyidea.util.getIndent
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import kotlin.math.min

/** At this length, we put table cells on their own line. */
const val LINE_LENGTH = 80

/**
 * Align spaces to the right of &
 */
fun rightTableSpaceAlign(latexCommonSettings: CommonCodeStyleSettings, parent: ASTBlock, left: ASTBlock): Spacing? {
    // Only add spaces after &, unless escaped
    if (left.node?.text?.endsWith("&") == false) return null
    if (left.node?.text?.endsWith("\\&") == true) return null

    if (parent.node?.psi?.firstParentOfType(LatexEnvironmentContent::class)
        ?.firstParentOfType(LatexEnvironment::class)?.getEnvironmentName() !in EnvironmentMagic.getAllTableEnvironments(
                parent.node?.psi?.project ?: ProjectManager.getInstance().defaultProject
            )
    ) return null

    return createSpacing(
        minSpaces = 1,
        maxSpaces = 1,
        minLineFeeds = 0,
        keepLineBreaks = latexCommonSettings.KEEP_LINE_BREAKS,
        keepBlankLines = latexCommonSettings.KEEP_BLANK_LINES_IN_CODE
    )
}

/**
 * Align spaces to the left of & or \\
 */
fun leftTableSpaceAlign(latexCommonSettings: CommonCodeStyleSettings, parent: ASTBlock, right: ASTBlock): Spacing? {
    // Check if parent is in environment content of a table environment
    val contentElement = parent.node?.psi?.firstParentOfType(LatexEnvironmentContent::class)
    val project = parent.node?.psi?.project ?: ProjectManager.getInstance().defaultProject
    if (contentElement?.firstParentOfType(LatexEnvironment::class)?.getEnvironmentName() !in EnvironmentMagic.getAllTableEnvironments(
            project
        )
    ) return null

    val tableLineSeparator = "\\\\"
    if (right.node?.text?.startsWith("&") == false && right.node?.text != tableLineSeparator) return null

    val content = contentElement?.text ?: return null
    val contentLines = content.split(tableLineSeparator)
        .mapNotNull { if (it.isBlank()) null else it + tableLineSeparator }
        .toMutableList()
    if (contentLines.size < 2) return null
    val indent = content.split("\n").map { "\n" + it }.getOrNull(1)?.getIndent() ?: return null

    // Fix environment content not starting with indent
    contentLines[0] = indent + contentLines.first()

    val absoluteAmpersandIndicesPerLine = getAmpersandOffsets(contentElement.textOffset, indent, contentLines)

    val contentWithoutRules = removeRules(content, tableLineSeparator)

    var spaces =
        getNumberOfSpaces(contentWithoutRules, tableLineSeparator, right, absoluteAmpersandIndicesPerLine, indent)
            ?: return null

    // Convert a -1 return value to a line feed.
    var lineFeeds = 0
    // I think checking for 'ensure right margin is not exceeded' makes more sense, but don't know which option that is
    if (spaces < 0 && latexCommonSettings.WRAP_ON_TYPING > 0) {
        spaces = 0
        lineFeeds = 1
    }

    return createSpacing(
        minSpaces = spaces,
        maxSpaces = spaces,
        minLineFeeds = lineFeeds,
        keepLineBreaks = latexCommonSettings.KEEP_LINE_BREAKS,
        keepBlankLines = latexCommonSettings.KEEP_BLANK_LINES_IN_CODE
    )
}

/**
 * Find the current absolute offset of all the ampersands
 */
fun getAmpersandOffsets(contentTextOffset: Int, indent: String, contentLines: MutableList<String>): List<List<Int>> {
    // We added fake content, but it doesn't count for the text offset
    var currentOffset = contentTextOffset - indent.length

    return contentLines.map { line ->
        val indices = mutableListOf<Int>()
        line.withIndex().forEach { (i, it) ->
            // Do not count escaped ampersands: \&
            if ((it == '&') && (i == 0 || line[i - 1] != '\\')) indices.add(currentOffset)
            if (it == '\\' && if (i < line.length - 1) line[i + 1] == '\\' else false) indices.add(currentOffset)
            currentOffset++
        }
        indices.toList()
    }
}

/**
 * Remove lines without &, and everything after \\ to avoid confusing while aligning the &'s
 * Assumes users place things such as \hline or \midrule after the \\ or on a separate line
 */
fun removeRules(content: String, tableLineSeparator: String): String {
    return content.split("\n").mapNotNull { line ->
        when {
            line.contains(tableLineSeparator) -> {
                // remove everything after \\
                line.split(tableLineSeparator).first() + tableLineSeparator
            }
            line.count { it == '&' } == 0 -> null
            else -> line
        }
    }.joinToString("\n")
}

fun getNumberOfSpaces(
    contentWithoutRules: String,
    tableLineSeparator: String,
    right: ASTBlock,
    absoluteAmpersandIndicesPerLine: List<List<Int>>,
    indent: String
): Int? {
    val contentLinesWithoutRules = contentWithoutRules.split(tableLineSeparator)
        .mapNotNull { if (it.isBlank()) null else it + tableLineSeparator }
        .toMutableList()
    if (contentLinesWithoutRules.isEmpty()) return null
    contentLinesWithoutRules[0] = indent + contentLinesWithoutRules.first()

    val relativeIndices = removeExtraSpaces(contentLinesWithoutRules)

    val spacesPerCell = getSpacesPerCell(relativeIndices, contentLinesWithoutRules)

    return getSpacesForRightBlock(right, absoluteAmpersandIndicesPerLine, spacesPerCell, relativeIndices)
}

/**
 * Remove all extra spaces and remember how many we removed
 *
 * @return List of lists, each list is a list of indices (the offset in the line for this ampersand)
 */
private fun removeExtraSpaces(contentLinesWithoutRules: MutableList<String>): List<List<Int>> {
    return contentLinesWithoutRules.map { line ->
        // (relative index after removing spaces, number of spaces removed on this line before this ampersand)
        val indices = mutableListOf<Int>()
        var removedSpaces = 0
        line.withIndex().forEach { (i, value) ->
            when {
                // Ignore escaped ampersands
                value == '&' && if (i > 0) line[i - 1] != '\\' else true -> {
                    indices += i - removedSpaces
                }
                value == '\\' && if (i < line.length - 1) line[i + 1] == '\\' else false -> {
                    indices += i - removedSpaces
                }
                value in setOf(' ', '\n') -> {
                    if (i > 0 && i < line.length - 1) {
                        // Spaces after an ignored ampersand are not removed
                        val isAfterSpaceOrSeparator = (line[i - 1] in setOf(' ', '&', '\n') && (i < 2 || line[i - 2] != '\\'))
                        val isBeforeSpaceOrSeparator = line[i + 1] in setOf(' ', '&', '\n')
                        val isBeforeDoubleBackslash = i < line.length - 2 && line[i + 1] == '\\' && line[i + 2] == '\\'
                        if (isAfterSpaceOrSeparator || isBeforeSpaceOrSeparator || isBeforeDoubleBackslash) removedSpaces++
                    }
                }
                else -> {
                }
            }
        }
        indices.toList()
    }
}

/**
 * Get the desired index of the first &, second &, etc, relative to the line start.
 *
 * Get the number of spaces that has to be added in each cell.
 * 1 when we should do nothing, except the single required space.
 * Indexed by line, then by level.
 */
private fun getSpacesPerCell(
    relativeIndices: List<List<Int>>,
    contentLinesWithoutRules: MutableList<String>
): List<List<Int>> {
    val nrLevels = relativeIndices.maxOfOrNull { it.size } ?: 0

    // If we are on a on a table line that is split over multiple `physical' lines,
    // ignore this line in all computations.
    fun String?.ignore(): Boolean {
        if (this == null) return false
        val containsNewLines = split("\n").filter { it.isNotBlank() }.size > 1
        val lessCells = count { it == '&' } + 1 < nrLevels
        return containsNewLines || lessCells
    }

    // In each line, compute the width of each cell.
    val cellWidthsPerLine = relativeIndices.mapIndexed { i, line ->
        if (contentLinesWithoutRules.getOrNull(i)?.ignore() == true) List(nrLevels) { 0 }
        else listOf(line.first()) + line.zipWithNext { a, b ->
            // Empty cells should have width 0.
            (b - a).let {
                if (it == 1) 0 else it
            }
        }
    }

    // Take the maximum width of each i-th cell over all lines.
    val cellWidths = cellWidthsPerLine.first().indices.map { level ->
        cellWidthsPerLine.mapNotNull { it.getOrNull(level) }.maxOrNull() ?: return mutableListOf()
    }

    // The number of spaces that has to be added to this cell is the
    // difference with the largest cell in this column, plus one additional
    // space that has to be added to every cell.
    return cellWidthsPerLine.mapIndexed { i, line ->
        line.mapIndexed { level, cellWidth ->
            if (contentLinesWithoutRules.getOrNull(i)?.ignore() == true) 1
            // Add 1 for the space that always has to be there.
            else cellWidths.getOrNull(level)?.minus(cellWidth)?.plus(1) ?: 1
        }
    }
}

/**
 * Find level of 'right' block (which is a & or \\).
 *
 * @return Number of spaces, -1 if a newline is required.
 */
private fun getSpacesForRightBlock(
    right: ASTBlock,
    absoluteAmpersandIndicesPerLine: List<List<Int>>,
    spacesPerCell: List<List<Int>>,
    relativeIndices: List<List<Int>>
): Int? {
    val rightElementOffset = right.node?.psi?.textOffset ?: return null
    // Current and desired index relative to line start, e.g. in case 'asdf__&` is desired, index 6
    for ((i, absoluteIndices) in absoluteAmpersandIndicesPerLine.withIndex()) {
        for (level in absoluteIndices.indices) {
            // Try to find the offset of the right & in the list of all & offsets
            if (absoluteIndices[level] == rightElementOffset) {
                // For very long lines, it's a lot more readable to start & on a new line instead of inserting a whole bunch of spaces
                // Make sure not to only put the \\ on a new line
                val didPreviousCellGetNewline = if (level == 0) true else (
                    relativeIndices.getOrNull(i)?.getOrNull(level - 1)
                        ?: 0
                    ) > LINE_LENGTH
                if ((
                    relativeIndices.getOrNull(i)?.getOrNull(level)
                        ?: 0
                    ) > LINE_LENGTH && (didPreviousCellGetNewline || level < absoluteIndices.size - 1)
                ) return -1
                return spacesPerCell.getOrNull(min(i, spacesPerCell.size - 1))?.getOrNull(level)
            }
        }
    }
    return null
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.util.indexing.FileBasedIndex
import com.intellij.util.indexing.FileContent
import nl.hannahsten.texifyidea.file.ClassFileType
import nl.hannahsten.texifyidea.file.LatexSourceFileType
import nl.hannahsten.texifyidea.file.StyleFileType
import nl.hannahsten.texifyidea.util.containsAny
import nl.hannahsten.texifyidea.util.startsWithAny

/**
 * Extract docs and do some basic formatting on documentation strings found in dtx files.
 *
 * @author Thomas
 */
object LatexDocsRegexer {

    /**
     * Regexes and replacements which clean up the documentation.
     *
     * **test**
     * Arguments \[mop\]arg should be left in, because they are needed when adding to the autocomplete
     */
    private val formattingReplacers = listOf(
        // Commands to remove entirely,, making sure to capture in the argument nested braces
        Pair("""\\(cite|footnote)\{(\{[^}]*}|[^}])+?}\s*""".toRegex()) { "" },
        // \cs command from the doctools package
        Pair("""(?
[^|]|^)\\c[sn]\{(?<command>[^}]+?)}""".toRegex()) { result -> result.groups["pre"]?.value + "<tt>\\" + result.groups["command"]?.value + "</tt>" },
        // Other commands, except when in short verbatim
        Pair("""(?
[^|]|^)\\(?:textsf|textsc|cmd|pkg|env)\{(?<argument>(\{[^}]*}|[^}])+?)}""".toRegex()) { result -> result.groups["pre"]?.value + "<tt>" + result.groups["argument"]?.value + "</tt>" },
        // Replace \textbf with  tags
        Pair("""\\textbf\{(?<argument>(\{[^}]*}|[^}])+?)}""".toRegex()) { result -> "${result.groups["argument"]?.value}" },
        // Replace \emph and \textit with  tags
        Pair<Regex, (MatchResult) -> String>("""\\(textit|emph)\{(?<argument>(\{[^}]*}|[^}])+?)}""".toRegex()) { result -> "${result.groups["argument"]?.value}" },
        // Short verbatim, provided by ltxdoc
        Pair("""\|""".toRegex()) { "" },
        // While it is true that text reflows in the documentation popup, so we don't need linebreaks, often package authors include an environment or something else
        // which does depend on linebreaks to be readable, and therefore we keep linebreaks by default.
        Pair("""\n""".toRegex()) { "
" },
    )

    /**
     * Commands that indicate that the documentation of a macro has stopped (besides a newline).
     */
    private val stopsDocs = setOf("\\begin{macrocode}", "\\DescribeMacro")

    /**
     * Skip lines that start with one of these strings.
     */
    private val skipLines = arrayOf("\\changes")

    /**
     * Regex for the documentation lines itself.
     * Some things to note:
     * - Docs do not necessarily start on their own line
     * - We do use empty lines to guess where the docs end
     */
    private val docsAfterMacroRegex = """%?\h*(?<line>.+\n)""".toRegex()

    /**
     * Should format to valid HTML as used in the docs popup.
     * Only done when indexing, but it should still be fast because it can be done up to 28714 times for full TeX Live.
     */
    fun format(docs: String): String {
        var formatted = docs.trim()
        formattingReplacers.forEach { formatted = it.first.replace(formatted, it.second).trim() }
        return formatted.trim()
    }

    /**
     * Extract documentation keys and values from [inputData] based on [regex] and put them in [map].
     * Assumes that the regex contains groups with the name "key" or "key1" and "value".
     */
    fun getDocsByRegex(inputData: FileContent, map: MutableMap<String, String>, regex: Regex) {
        val macrosBeingOverloaded = mutableSetOf<String>()

        regex.findAll(inputData.contentAsText).forEach loop@{ macroWithDocsResult ->
            val key = macroWithDocsResult.groups["key"]?.value ?: macroWithDocsResult.groups["key1"]?.value ?: return@loop
            // The string that hopefully contains some documentation about the macro
            val containsDocs = macroWithDocsResult.groups["value"]?.value ?: ""

            // If we are overloading macros, just save this one to fill with documentation later.
            if (containsDocs.trim(' ', '%').startsWithAny("\\begin{macro}", "\\DescribeMacro")) {
                macrosBeingOverloaded.add(key)
            }
            else {
                var docs = ""
                // Strip the line prefixes and guess until where the documentation goes.
                run breaker@{
                    docsAfterMacroRegex.findAll(containsDocs).forEach { lineResult ->
                        val line = lineResult.groups["line"]?.value ?: return@forEach
                        if (line.trim().startsWithAny(*skipLines)) {
                            return@forEach
                        }
                        else if (!line.containsAny(stopsDocs) && line.trim(' ', '%').isNotBlank()) {
                            docs += " $line"
                        }
                        else {
                            return@breaker
                        }
                    }
                }

                // Avoid overwriting existing docs with an empty string
                val formatted = format(docs.trim())
                if (key in map.keys && formatted.isBlank()) return@loop

                map[key] = formatted
                if (macrosBeingOverloaded.isNotEmpty()) {
                    macrosBeingOverloaded.forEach { map[it] = format(docs.trim()) }
                    macrosBeingOverloaded.clear()
                }
            }
        }
    }

    /**
     * Determine which files to index.
     */
    val inputFilter = FileBasedIndex.InputFilter { file ->
        // sty files are included, because in some cases there are no dtx files, or the dtx files have a different name, so we use the sty file to find out which package should be imported.
        // This does mean we get many duplicates in the index.
        file.fileType is LatexSourceFileType || file.fileType is StyleFileType || file.fileType is ClassFileType
    }
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.util.indexing.DataIndexer
import com.intellij.util.indexing.FileContent

/**
 * Get the content we need from LaTeX Package source files.
 * @author Thomas
 */
class LatexExternalCommandDataIndexer : DataIndexer<String, String, FileContent> {

    companion object {

        /**
         * Capture the command being defined with the macro environment and the part directly after that that probably contains docs.
         * We limit to a certain length to avoid matching very large strings if the regex fails.
         * Then we use a separate regex to get the actual docs out of that string, matching lines until a certain stopping criterion.
         * Use a positive lookahead to find overlapping matchings if they fall within the 'docs' range.
         */
        val macroWithDocsRegex =
            """(?=\\begin\{macro}\{(?<key>\\[a-zA-Z_:]+)}\s*(?<value>[\s\S]{0,500}))""".toRegex()

        /**
         * Documentation given by \DescribeMacro.
         * Supports a definition with or without braces.
         */
        val describeMacroRegex =
            """(?=\\DescribeMacro(?:(?<key>\\[a-zA-Z_:]+\*?)|\{(?<key1>\\[a-zA-Z_:]+\*?)})\s*(?<value>[\s\S]{0,500}))""".toRegex()

        // Avoid matching commands with @ in it by using an atomic group
        val directCommandDefinitionRegex = """\\(DeclareRobustCommand|newcommand|def)\*?(?:(?<key>(?>\\[a-zA-Z_:]+\*?)(?!@))|\{(?<key1>\\[a-zA-Z_:]+\*?)})(?<value>)""".toRegex()

        /**
         * See sourc2e.pdf:
         *
         * ```\DeclareTextCommand{command}{encoding}[number][default]{commands}```
         * This command is like \newcommand, except that it defines a command which is specific to one encoding.
         *
         * Similar: \DeclareTextSymbol, \DeclareTextAccent, \DeclareTextComposite, \DeclareTextCompositeCommand
         */
        val declareTextSymbolRegex = """\\DeclareText(?:Symbol|Accent)\{(?<key>[^}]+)}(?<value>\{(?<encoding>[^}]+)}(?:.+)*?\{(?<slot>[^}]+)})""".toRegex()
        val declareTextCommandRegex = """\\DeclareTextCommand\{(?<key>[^}]+)}\{(?<value>[^}]+)}""".toRegex()
    }

    override fun map(inputData: FileContent): MutableMap<String, String> {
        val map = mutableMapOf<String, String>()

        listOf(
            macroWithDocsRegex,
            describeMacroRegex,
            directCommandDefinitionRegex,
            directCommandDefinitionRegex,
            declareTextSymbolRegex,
            declareTextCommandRegex,
        ).forEach { LatexDocsRegexer.getDocsByRegex(inputData, map, it) }

        return map
    }
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.util.indexing.*
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor

/**
 * Index of all defined commands in source files (dtx) of LaTeX packages.
 * The actual indexing is done by [LatexExternalCommandDataIndexer].
 * The paths that have to be indexed are given by [LatexIndexableSetContributor].
 *
 * When developing, the index is present in build/idea-sandbox/system-test/index
 *
 * Key: LaTeX command (with backslash).
 * Value: Documentation string.
 *
 * @author Thomas
 */
class LatexExternalCommandIndex : FileBasedIndexExtension<String, String>() {

    companion object {

        val id = ID.create<String, String>("nl.hannahsten.texifyidea.external.commands")
    }

    private val indexer = LatexExternalCommandDataIndexer()

    override fun getName(): ID<String, String> {
        return id
    }

    override fun getIndexer(): DataIndexer<String, String, FileContent> {
        return indexer
    }

    override fun getKeyDescriptor(): KeyDescriptor<String> {
        return EnumeratorStringDescriptor.INSTANCE
    }

    override fun getValueExternalizer(): DataExternalizer<String> {
        return EnumeratorStringDescriptor.INSTANCE
    }

    override fun getVersion() = 1

    override fun getInputFilter(): FileBasedIndex.InputFilter {
        return LatexDocsRegexer.inputFilter
    }

    override fun dependsOnFileContent() = true

    override fun traceKeyHashToVirtualFileMapping() = true
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.util.indexing.DataIndexer
import com.intellij.util.indexing.FileContent
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * Similar to [LatexExternalCommandDataIndexer] but for environments.
 *
 * @author Thomas
 */
class LatexExternalEnvironmentDataIndexer : DataIndexer<String, String, FileContent> {

    /**
     * See [LatexExternalCommandDataIndexer].
     */
    private val macroWithDocsRegex = """(?=\\begin\{environment}\{(?<key>[a-zA-Z_:]+)}\s*(?<value>[\s\S]{0,500}))""".toRegex()

    private val describeMacroRegex =
        """(?=\\DescribeEnv\{?(?<key>[a-zA-Z_:]+\*?)}?\s*(?<value>[\s\S]{0,500}))""".toRegex()

    // e.g. \newenvironment{proof}{...} \newenvironment|\newtheorem|\NewDocumentEnvironment|\DeclareDocumentEnvironment|\ProvideDocumentEnvironment
    private val directDefinitionRegex = """(${CommandMagic.environmentDefinitions.joinToString("|").replace("\\", "\\\\")})\*?\{(?<key>[a-zA-Z_:]+)}(?<value>)""".toRegex()

    override fun map(inputData: FileContent): MutableMap<String, String> {
        val map = mutableMapOf<String, String>()

        LatexDocsRegexer.getDocsByRegex(inputData, map, macroWithDocsRegex)
        LatexDocsRegexer.getDocsByRegex(inputData, map, describeMacroRegex)
        LatexDocsRegexer.getDocsByRegex(inputData, map, directDefinitionRegex)
        return map
    }
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.util.indexing.*
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor

/**
 * Similar to [LatexExternalCommandIndex] but for environments.
 *
 * @author Thomas
 */
class LatexExternalEnvironmentIndex : FileBasedIndexExtension<String, String>() {

    companion object {

        val id = ID.create<String, String>("nl.hannahsten.texifyidea.external.environments")
    }

    private val indexer = LatexExternalEnvironmentDataIndexer()

    override fun getName(): ID<String, String> {
        return id
    }

    override fun getIndexer(): DataIndexer<String, String, FileContent> {
        return indexer
    }

    override fun getKeyDescriptor(): KeyDescriptor<String> {
        return EnumeratorStringDescriptor.INSTANCE
    }

    override fun getValueExternalizer(): DataExternalizer<String> {
        return EnumeratorStringDescriptor.INSTANCE
    }

    override fun getVersion() = 0

    override fun getInputFilter(): FileBasedIndex.InputFilter {
        return LatexDocsRegexer.inputFilter
    }

    override fun dependsOnFileContent() = true
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.rd.util.concurrentMapOf
import nl.hannahsten.texifyidea.algorithm.DFS
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.files.removeFileExtension

/**
 * Compute and cache for each package style file all the other style files it includes (directly or indirectly).
 * This is cached because even though it uses [LatexExternalPackageInclusionIndex], the computation could still
 * take some time and is thus cached in memory as it could potentially be called on every letter typed.
 */
object LatexExternalPackageInclusionCache {

    private val cache = concurrentMapOf<LatexPackage, Set<LatexPackage>>()

    /**
     * Map every LaTeX package style file to all the style files it includes, directly or indirectly.
     */
    @Synchronized
    fun getAllPackageInclusions(project: Project): Map<LatexPackage, Set<LatexPackage>> {
        if (cache.isNotEmpty() || DumbService.isDumb(project)) return cache

        val directChildren = mutableMapOf<LatexPackage, MutableSet<LatexPackage>>()

        // Get direct children from the index
        FileBasedIndex.getInstance().getAllKeys(LatexExternalPackageInclusionIndex.id, project).forEach { indexKey ->
            FileBasedIndex.getInstance().processValues(
                LatexExternalPackageInclusionIndex.id, indexKey, null, { file, _ ->
                val key = LatexPackage(file.name.removeFileExtension())
                directChildren[key] = directChildren.getOrDefault(key, mutableSetOf()).also { it.add(LatexPackage((indexKey))) }
                true
            },
                GlobalSearchScope.everythingScope(project)
            )
        }

        // Do some DFS for indirect inclusions
        for (latexPackage in directChildren.keys) {
            cache[latexPackage] = DFS(latexPackage) { parent -> directChildren[parent] ?: emptySet() }.execute()
        }

        return cache
    }

    /**
     * Given a certain set of [LatexPackage]s, return the set of all those packages plus all the packages they directly or indirectly include.
     */
    fun getAllIndirectlyIncludedPackages(packages: Collection<LatexPackage>, project: Project): Set<LatexPackage> {
        val result = packages.toMutableSet()
        val allInclusions = getAllPackageInclusions(project)
        for (latexPackage in packages) {
            result.addAll(allInclusions[latexPackage] ?: emptySet())
        }
        return result
    }
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.util.indexing.DataIndexer
import com.intellij.util.indexing.FileContent

/**
 * Index file contents for [LatexExternalPackageInclusionIndex].
 */
class LatexExternalPackageInclusionDataIndexer : DataIndexer<String, String, FileContent> {

    companion object {

        val packageInclusionsRegex = """\\(RequirePackage|usepackage)(\[[^]]+])?\{(?<package>[^}]+)}""".toRegex()
    }

    override fun map(inputData: FileContent): MutableMap<String, String> {
        val result = mutableMapOf<String, String>()

        packageInclusionsRegex.findAll(inputData.contentAsText).forEach {
            val packages = it.groups["package"]?.value ?: return@forEach
            for (key in packages.split(",")) {
                result[key] = inputData.fileName
            }
        }

        return result
    }
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.util.indexing.*
import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.KeyDescriptor
import nl.hannahsten.texifyidea.file.ClassFileType
import nl.hannahsten.texifyidea.file.StyleFileType

/**
 * Indexes which packages and classes load which other packages.
 * Note that with 'package' we mean here an actual .sty file as you would call in a \usepackage, which may be
 * named different from the ctan package it is contained in.
 *
 * Key: Package name, as it appears in a \RequirePackage-like statement
 * Value: not used. Could possibly be used for metadata about the inclusion (like optional parameters). Currently it is the file name for easier debugging, and as such it should be read package "key" is included by package "value".
 *
 * See [nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages.LatexMissingImportInspection].
 */
class LatexExternalPackageInclusionIndex : FileBasedIndexExtension<String, String>() {

    companion object {

        val id = ID.create<String, String>("nl.hannahsten.texifyidea.external.package.inclusions")
    }

    private val indexer = LatexExternalPackageInclusionDataIndexer()

    override fun getName(): ID<String, String> {
        return id
    }

    override fun getIndexer(): DataIndexer<String, String, FileContent> {
        return indexer
    }

    override fun getKeyDescriptor(): KeyDescriptor<String> {
        return EnumeratorStringDescriptor.INSTANCE
    }

    override fun getValueExternalizer(): DataExternalizer<String> {
        return EnumeratorStringDescriptor.INSTANCE
    }

    override fun getVersion() = 2

    override fun getInputFilter(): FileBasedIndex.InputFilter {
        return FileBasedIndex.InputFilter {
            it.fileType is StyleFileType || it.fileType is ClassFileType
        }
    }

    override fun dependsOnFileContent() = true
}

package nl.hannahsten.texifyidea.index.file

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.indexing.IndexableSetContributor
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import nl.hannahsten.texifyidea.util.Log
import nl.hannahsten.texifyidea.util.isTestProject
import org.codehaus.plexus.archiver.ArchiverException
import org.codehaus.plexus.archiver.tar.TarBZip2UnArchiver
import org.codehaus.plexus.archiver.tar.TarXZUnArchiver
import java.io.File
import java.nio.file.Files
import java.nio.file.Path

/**
 * Specify the paths that have to be indexed for the [LatexExternalCommandIndex].
 */
class LatexIndexableSetContributor : IndexableSetContributor() {

    private var extractedFiles = false

    override fun getAdditionalProjectRootsToIndex(project: Project): MutableSet<VirtualFile> {
        // Avoid indexing in tests
        if (project.isTestProject()) {
            return mutableSetOf()
        }

        if (!TexifySettings.getInstance().enableExternalIndex) return mutableSetOf()

        // Add source files
        val roots = LatexSdkUtil.getSdkSourceRoots(project) { sdk, homePath -> sdk.getDefaultSourcesPath(homePath) }.toMutableSet()
        // Check if we possibly need to extract files first
        Log.debug("Indexing source roots $roots")
        for (root in roots) {
            if (root.path.contains("MiKTeX", ignoreCase = true) && !extractedFiles) {
                try {
                    if (!extractMiktexFiles(root)) return mutableSetOf()
                }
                catch (e: ArchiverException) {
                    // Ignore permission errors, nothing we can do about that
                    Log.debug("Exception when trying to extract MiKTeX source files: ${e.message}")
                    return mutableSetOf()
                }
            }
        }

        // Add style files (used in e.g. LatexExternalPackageInclusionIndex)
        // Unfortunately, since .sty is a LaTeX file type, these will all be parsed, which will take an enormous amount of time.
        // Note that using project-independent getAdditionalRootsToIndex does not fix this
        roots.addAll(LatexSdkUtil.getSdkSourceRoots(project) { sdkType, homePath -> sdkType.getDefaultStyleFilesPath(homePath) })

        return roots
    }

    /**
     * MiKTeX keeps the dtx files in tar.xz/tar.bz2 files, so we have to extract them ourselves.
     * We could check if the target files exist and in that case not extract again, however then we would never update packages
     * Because maintaining extraction dates seems a bit too much work for now, just extract again after reboot only
     *
     * @return If succeeded.
     */
    private fun extractMiktexFiles(root: VirtualFile): Boolean {
        val txArchiver = TarXZUnArchiver()
        File(root.path).list { _, name -> name.endsWith("tar.xz") }?.forEach { zipName ->
            txArchiver.sourceFile = File(root.path, zipName)
            // Note that by keeping the target path the same for everything, some packages will install in source/latex and some in source/latex/latex depending on how they were zipped
            val destination = File(root.path, "latex")

            // If the user has e.g. a MiKTeX admin install, we do not have rights to extract zips
            if (!Files.isWritable(Path.of(root.path))) {
                extractedFiles = true
                Log.debug("MiKTeX installation path ${root.path} is not writable, cannot extract sources")
                return false
            }

            // Try to create if not exists
            if (!destination.exists() && !destination.mkdir()) {
                extractedFiles = true
                Log.debug("Could not create destination directory ${destination.absolutePath}")
                return false
            }

            txArchiver.destDirectory = destination
            txArchiver.extract()
        }
        val bz2Archiver = TarBZip2UnArchiver()
        File(root.path).list { _, name -> name.endsWith("tar.bz2") }?.forEach { zipName ->
            bz2Archiver.sourceFile = File(root.path, zipName)
            bz2Archiver.destDirectory = File(root.path, "latex")
            bz2Archiver.extract()
        }
        extractedFiles = true

        return true
    }

    override fun getAdditionalRootsToIndex() = mutableSetOf<VirtualFile>()
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.NamedStub
import com.intellij.psi.stubs.StubElement
import nl.hannahsten.texifyidea.psi.BibtexEntry

interface BibtexEntryStub : StubElement<BibtexEntry?>, NamedStub<BibtexEntry?> {

    val title: String
    val authors: List<String?>
    val year: String
    val identifier: String
    override fun getName(): String? {
        return identifier
    }
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.*
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.impl.BibtexEntryImpl
import nl.hannahsten.texifyidea.util.parser.getAuthors
import nl.hannahsten.texifyidea.util.parser.getIdentifier
import nl.hannahsten.texifyidea.util.parser.getTitle
import nl.hannahsten.texifyidea.util.parser.getYear

open class BibtexEntryStubElementType(debugName: String) : IStubElementType<BibtexEntryStub, BibtexEntry>(debugName, BibtexLanguage) {

    override fun createPsi(stub: BibtexEntryStub): BibtexEntry {
        return BibtexEntryImpl(stub, this)
    }

    override fun serialize(stub: BibtexEntryStub, dataStream: StubOutputStream) {
        dataStream.writeName(stub.identifier)
        dataStream.writeName(stub.title)
        dataStream.writeName(stub.authors.joinToString(";"))
        dataStream.writeName(stub.year)
    }

    override fun deserialize(dataStream: StubInputStream, parentStub: StubElement<*>?): BibtexEntryStub {
        val name = dataStream.readName()?.string ?: ""
        val title = dataStream.readName()?.string ?: ""
        val authors = (dataStream.readName()?.string ?: "").split(";")
        val year = dataStream.readName()?.string ?: ""
        return BibtexEntryStubImpl(parentStub, this, name, authors, year, title)
    }

    override fun createStub(entry: BibtexEntry, parentStub: StubElement<*>?): BibtexEntryStub {
        return BibtexEntryStubImpl(parentStub, this, entry.getIdentifier(), entry.getAuthors(), entry.getYear(), entry.getTitle())
    }

    override fun getExternalId() = "texify.bibtex." + super.toString()

    override fun indexStub(stub: BibtexEntryStub, sink: IndexSink) {
        sink.occurrence(BibtexEntryIndex().key, stub.name ?: "")
    }
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.IStubElementType
import com.intellij.psi.stubs.NamedStubBase
import com.intellij.psi.stubs.StubElement
import nl.hannahsten.texifyidea.psi.BibtexEntry

data class BibtexEntryStubImpl(
    val parent: StubElement<*>?,
    val elementType: IStubElementType<BibtexEntryStub, BibtexEntry>,
    val myIdentifier: String,
    val myAuthors: List<String>,
    val myYear: String,
    val myTitle: String
) : NamedStubBase<BibtexEntry>(parent, elementType, myIdentifier), BibtexEntryStub {

    override val identifier = myIdentifier

    override val authors = myAuthors

    override val title = myTitle

    override val year = myYear

    override fun getName() = myIdentifier

    override fun toString(): String {
        return "BibtexEntryStubImpl(myIdentifier='$myIdentifier', myAuthors=$myAuthors, myYear='$myYear', myTitle='$myTitle')"
    }
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.NamedStub
import com.intellij.psi.stubs.StubElement
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * @author Hannah Schellekens
 */
interface LatexCommandsStub : StubElement<LatexCommands?>, NamedStub<LatexCommands?> {

    val commandToken: String
    val requiredParams: List<String>
    val optionalParams: Map<String, String>
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.openapi.project.Project
import com.intellij.psi.stubs.*
import com.intellij.testFramework.LightVirtualFile
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.index.*
import nl.hannahsten.texifyidea.index.file.LatexIndexableSetContributor
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.impl.LatexCommandsImpl
import nl.hannahsten.texifyidea.util.parser.toStringMap
import nl.hannahsten.texifyidea.util.getIncludeCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import java.io.IOException
import java.util.regex.Pattern
import java.util.stream.Collectors

/**
 * @author Hannah Schellekens
 */
class LatexCommandsStubElementType(debugName: String) :
    IStubElementType<LatexCommandsStub, LatexCommands>(debugName, LatexLanguage) {

    override fun createPsi(latexCommandsStub: LatexCommandsStub): LatexCommands {
        return object : LatexCommandsImpl(latexCommandsStub, this) {
            init {
                this.name = latexCommandsStub.name!!
            }
        }
    }

    override fun createStub(latexCommands: LatexCommands, parent: StubElement<*>?): LatexCommandsStub {
        val commandToken = latexCommands.commandToken.text
        val requiredParameters = latexCommands.getRequiredParameters()
        val optionalParameters: Map<String, String> =
            latexCommands.getOptionalParameterMap().toStringMap()
        return LatexCommandsStubImpl(
            parent!!, this,
            commandToken,
            requiredParameters,
            optionalParameters
        )
    }

    // Should equal externalIdPrefix from registration in index.xml plus field name in LatexStubElementTypes
    override fun getExternalId() = "texify.latex." + super.toString()

    @Throws(IOException::class)
    override fun serialize(
        latexCommandsStub: LatexCommandsStub,
        stubOutputStream: StubOutputStream
    ) {
        stubOutputStream.writeName(latexCommandsStub.name)
        stubOutputStream.writeName(serialiseRequired(latexCommandsStub))
        stubOutputStream.writeName(serialiseOptional(latexCommandsStub))
    }

    @Throws(IOException::class)
    override fun deserialize(stubInputStream: StubInputStream, parent: StubElement<*>): LatexCommandsStub {
        val name = stubInputStream.readName().toString()
        val required = deserialiseList(stubInputStream.readName().toString())
        val optional = deserializeMap(stubInputStream.readName().toString())
        return LatexCommandsStubImpl(
            parent, this,
            name,
            required,
            optional
        )
    }

    private fun deserializeMap(fromString: String): Map<String, String> {
        val keyValuePairs = deserialiseList(fromString)
        return keyValuePairs.filter { it.isNotEmpty() }.associate {
            val parts = it.split(KEY_VALUE_SEPARATOR)
            parts[0] to parts[1]
        }
    }

    override fun indexStub(latexCommandsStub: LatexCommandsStub, indexSink: IndexSink) {
        // We do not want all the commands from all package source files in this index, because
        // then we end up with 200k keys for texlive full, but we need to iterate over all keys
        // every time we need to get e.g. all commands in a file, so that would be too slow.
        // Therefore, we check if the indexing of this file was caused by being in an extra project root or not
        // It seems we cannot make a distinction that we do want to index with LatexExternalCommandIndex but not this index
        // Unfortunately, this seems to make indexing five times slower
        val pathOfCurrentlyIndexedFile = (latexCommandsStub.psi?.containingFile?.viewProvider?.virtualFile as? LightVirtualFile)?.originalFile?.path

        // If any of the sdk source roots is part of the currently indexed path, don't index the file
        if (getAdditionalProjectRoots(latexCommandsStub.psi?.project).any { pathOfCurrentlyIndexedFile?.contains(it) == true }) {
            return
        }

        val token = latexCommandsStub.commandToken
        indexSinkOccurrence(indexSink, LatexCommandsIndex, token)
        if (token in getIncludeCommands()) {
            indexSinkOccurrence(indexSink, LatexIncludesIndex, token)
        }
        if (token in CommandMagic.definitions) {
            indexSinkOccurrence(indexSink, LatexDefinitionIndex, token)
        }
        if (token in CommandMagic.labelAsParameter && "label" in latexCommandsStub.optionalParams) {
            val label = latexCommandsStub.optionalParams["label"]!!
            indexSinkOccurrence(indexSink, LatexParameterLabeledCommandsIndex, label)
        }
        if (token in CommandMagic.glossaryEntry && latexCommandsStub.requiredParams.isNotEmpty()) {
            indexSinkOccurrence(indexSink, LatexGlossaryEntryIndex, latexCommandsStub.requiredParams[0])
        }
    }

    private fun deserialiseList(string: String): List<String> {
        return LIST_ELEMENT_SEPARATOR.splitAsStream(string)
            .collect(Collectors.toList())
    }

    private fun serialiseRequired(stub: LatexCommandsStub): String {
        return java.lang.String.join(LIST_ELEMENT_SEPARATOR.pattern(), stub.requiredParams)
    }

    private fun serialiseOptional(stub: LatexCommandsStub): String {
        val keyValuePairs = stub.optionalParams.map { "${it.key}=${it.value}" }
        return java.lang.String.join(LIST_ELEMENT_SEPARATOR.pattern(), keyValuePairs)
    }

    companion object {

        private val LIST_ELEMENT_SEPARATOR =
            Pattern.compile("\u1923\u9123\u2d20 hello\u0012")
        private val KEY_VALUE_SEPARATOR = "=".toRegex()

        private var projectRootsCache: List<String>? = null

        fun getAdditionalProjectRoots(project: Project?): List<String> {
            if (projectRootsCache == null && project != null) {
                projectRootsCache = LatexIndexableSetContributor().getAdditionalProjectRootsToIndex(project).map { it.path }
            }
            return projectRootsCache ?: emptyList()
        }
    }
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.IStubElementType
import com.intellij.psi.stubs.NamedStubBase
import com.intellij.psi.stubs.StubElement
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * @author Hannah Schellekens
 */
class LatexCommandsStubImpl(
    parent: StubElement<*>,
    elementType: IStubElementType<*, *>,
    override val commandToken: String,
    override val requiredParams: List<String>,
    override val optionalParams: Map<String, String>
) : NamedStubBase<LatexCommands>(parent, elementType, commandToken), LatexCommandsStub {

    override fun getName() = commandToken

    override fun toString() = "LatexCommandsStubImpl{commandToken='$commandToken', requiredParams=$requiredParams, optionalParams=$optionalParams}"
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.StubElement
import nl.hannahsten.texifyidea.psi.LatexEnvironment

interface LatexEnvironmentStub : StubElement<LatexEnvironment?> {

    val environmentName: String
    val label: String
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.*
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.index.LatexEnvironmentsIndex
import nl.hannahsten.texifyidea.index.LatexParameterLabeledEnvironmentsIndex
import nl.hannahsten.texifyidea.index.indexSinkOccurrence
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.getEnvironmentName
import nl.hannahsten.texifyidea.psi.getLabel
import nl.hannahsten.texifyidea.psi.impl.LatexEnvironmentImpl
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import java.io.IOException

open class LatexEnvironmentStubElementType(debugName: String) : IStubElementType<LatexEnvironmentStub, LatexEnvironment>(debugName, LatexLanguage) {

    override fun createPsi(stub: LatexEnvironmentStub): LatexEnvironment {
        return LatexEnvironmentImpl(stub, this)
    }

    override fun createStub(psi: LatexEnvironment, parentStub: StubElement<*>): LatexEnvironmentStub {
        return LatexEnvironmentStubImpl(parentStub, this, psi.getEnvironmentName(), psi.getLabel() ?: "")
    }

    override fun getExternalId() = "texify.latex." + super.toString()

    @Throws(IOException::class)
    override fun serialize(stub: LatexEnvironmentStub, dataStream: StubOutputStream) {
        dataStream.writeName(stub.environmentName)
        dataStream.writeName(stub.label)
    }

    @Throws(IOException::class)
    override fun deserialize(dataStream: StubInputStream, parentStub: StubElement<*>): LatexEnvironmentStub {
        val envName = dataStream.readName()?.string ?: ""
        val label = dataStream.readName()?.string ?: ""
        return LatexEnvironmentStubImpl(parentStub, this, envName, label)
    }

    override fun indexStub(stub: LatexEnvironmentStub, sink: IndexSink) {
        indexSinkOccurrence(sink, LatexEnvironmentsIndex, stub.environmentName)

        // only record environments with a label in the optional parameters
        if (stub.label.isNotEmpty() && EnvironmentMagic.labelAsParameter.contains(stub.environmentName)) {
            indexSinkOccurrence(sink, LatexParameterLabeledEnvironmentsIndex, stub.label)
        }
    }
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.IStubElementType
import com.intellij.psi.stubs.StubBase
import com.intellij.psi.stubs.StubElement
import nl.hannahsten.texifyidea.psi.LatexEnvironment

data class LatexEnvironmentStubImpl(
    val parent: StubElement<*>,
    val elementType: IStubElementType<LatexEnvironmentStub, LatexEnvironment>,
    override val environmentName: String,
    override val label: String
) :
    StubBase<LatexEnvironment>(parent, elementType), LatexEnvironmentStub

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.StubElement
import nl.hannahsten.texifyidea.psi.LatexMagicComment

interface LatexMagicCommentStub : StubElement<LatexMagicComment?> {

    val key: String
    val value: String?
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.*
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.index.LatexMagicCommentIndex
import nl.hannahsten.texifyidea.index.indexSinkOccurrence
import nl.hannahsten.texifyidea.psi.LatexMagicComment
import nl.hannahsten.texifyidea.psi.impl.LatexMagicCommentImpl
import nl.hannahsten.texifyidea.psi.key
import nl.hannahsten.texifyidea.psi.value
import java.io.IOException

open class LatexMagicCommentStubElementType(debugName: String) : IStubElementType<LatexMagicCommentStub, LatexMagicComment>(debugName, LatexLanguage) {

    override fun createPsi(stub: LatexMagicCommentStub): LatexMagicComment = LatexMagicCommentImpl(stub, this)

    override fun createStub(psi: LatexMagicComment, parentStub: StubElement<*>): LatexMagicCommentStub {
        return LatexMagicCommentStubImpl(parentStub, this, psi.key().toString(), psi.value())
    }

    override fun getExternalId() = "texify.latex." + super.toString()

    @Throws(IOException::class)
    override fun serialize(stub: LatexMagicCommentStub, dataStream: StubOutputStream) {
        dataStream.writeName(stub.key)
        dataStream.writeName(stub.value)
    }

    override fun deserialize(dataStream: StubInputStream, parentStub: StubElement<*>): LatexMagicCommentStub {
        val key = dataStream.readName()?.string ?: ""
        val value = dataStream.readName()?.string ?: ""
        return LatexMagicCommentStubImpl(parentStub, this, key, value)
    }

    override fun indexStub(stub: LatexMagicCommentStub, sink: IndexSink) {
        indexSinkOccurrence(sink, LatexMagicCommentIndex, stub.key)
    }
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.IStubElementType
import com.intellij.psi.stubs.StubBase
import com.intellij.psi.stubs.StubElement
import nl.hannahsten.texifyidea.psi.LatexMagicComment

class LatexMagicCommentStubImpl(
    val parent: StubElement<*>,
    val elementType: IStubElementType<LatexMagicCommentStub, LatexMagicComment>,
    override val key: String,
    override val value: String?
) : StubBase<LatexMagicComment>(parent, elementType), LatexMagicCommentStub

package nl.hannahsten.texifyidea.inspections.bibtex

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.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.createSmartPointer
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.getIncludedFiles
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * @author Sten Wessel
 */
open class BibtexDuplicateBibliographyInspection : TexifyInspectionBase() {

    // Manual override to match short name in plugin.xml
    override fun getShortName() = InsightGroup.BIBTEX.prefix + inspectionId

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "DuplicateBibliography"

    override fun getDisplayName() = "Same bibliography is included multiple times"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        // Chapterbib allows multiple bibliographies
        if (file.includedPackages().any { it == LatexPackage.CHAPTERBIB }) {
            return emptyList()
        }

        val descriptors = descriptorList()

        // Map each bibliography file to all the commands which include it
        val groupedIncludes = mutableMapOf<String, MutableList<LatexCommands>>()

        LatexIncludesIndex.getItemsInFileSet(file).asSequence()
            .filter { it.name == "\\bibliography" || it.name == "\\addbibresource" }
            .forEach { command ->
                for (fileName in command.getIncludedFiles(false).map { it.name }) {
                    groupedIncludes.getOrPut(fileName) { mutableListOf() }.add(command)
                }
            }

        groupedIncludes.asSequence()
            .filter { (_, commands) -> commands.size > 1 }
            .forEach { (fileName, commands) ->
                for (command in commands.distinct()) {
                    if (command.containingFile != file) continue

                    val parameterIndex = command.requiredParameter(0)?.indexOf(fileName) ?: break
                    if (parameterIndex < 0) break

                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            TextRange(parameterIndex, parameterIndex + fileName.length).shiftRight(command.commandToken.textLength + 1),
                            "Bibliography file '$fileName' is included multiple times",
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly,
                            RemoveOtherCommandsFix(fileName, commands.map { it.createSmartPointer() })
                        )
                    )
                }
            }

        return descriptors
    }

    /**
     * @author Sten Wessel
     */
    class RemoveOtherCommandsFix(private val bibName: String, private val commandsToFix: List<SmartPsiElementPointer<LatexCommands>>) :
        LocalQuickFix {

        override fun getFamilyName(): String {
            return "Remove other includes of $bibName"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val currentCommand = descriptor.psiElement as LatexCommands
            val documentManager = PsiDocumentManager.getInstance(project)

            // For all commands to be fixed, remove the matching bibName
            // Handle commands by descending offset, to make sure the replaceString calls work correctly
            for (command in commandsToFix.mapNotNull { it.element }.sortedByDescending { it.textOffset }) {
                val document = command.containingFile.document() ?: continue
                val param = command.parameterList.first()

                // If we handle the current command, find the first occurrence of bibName and leave it intact
                val firstBibIndex = if (command == currentCommand) {
                    param.text.trimRange(1, 1).splitToSequence(',').indexOfFirst { it.trim() == bibName }
                }
                else -1

                val replacement = param.text.trimRange(1, 1).splitToSequence(',')
                    // Parameter should stay if it is at firstBibIndex or some other bibliography file
                    .filterIndexed { i, it -> i <= firstBibIndex || it.trim() != bibName }
                    .joinToString(",", prefix = "{", postfix = "}")

                // When no arguments are left, just delete the command
                if (replacement.trimRange(1, 1).isBlank()) {
                    command.delete()
                }
                else {
                    document.replaceString(param.textRange, replacement)
                }
                documentManager.doPostponedOperationsAndUnblockDocument(document)
                documentManager.commitDocument(document)
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.bibtex

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.findAtLeast
import nl.hannahsten.texifyidea.util.includedPackages

/**
 * @author Hannah Schellekens
 */
open class BibtexDuplicateBibliographystyleInspection : TexifyInspectionBase() {

    // Manual override to match short name in plugin.xml
    override fun getShortName() = InsightGroup.BIBTEX.prefix + inspectionId

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "DuplicateBibliographystyle"

    override fun getDisplayName() = "Duplicate bibliography style commands"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        // Chapterbib allows multiple bibliographies
        if (file.includedPackages().any { it == LatexPackage.CHAPTERBIB }) {
            return mutableListOf()
        }

        val descriptors = descriptorList()

        // Check if a bibliography is present.
        val commands = file.commandsInFileSet()
        commands.find { it.name == "\\bibliography" } ?: return descriptors

        if (commands.findAtLeast(2) { it.name == "\\bibliographystyle" }) {
            file.commandsInFile().asSequence()
                .filter { it.name == "\\bibliographystyle" }
                .forEach {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            it,
                            TextRange(0, it.commandToken.textLength),
                            "\\bibliographystyle is already used elsewhere",
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly,
                            RemoveOtherCommandsFix
                        )
                    )
                }
        }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    object RemoveOtherCommandsFix : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Remove other \\bibliographystyle commands"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val file = command.containingFile

            file.commandsInFileSet().asSequence()
                .filter { it.name == "\\bibliographystyle" && it != command }
                .forEach {
                    it.delete()
                }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.bibtex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.findAtLeast
import nl.hannahsten.texifyidea.util.labels.findLatexCommandsLabels
import nl.hannahsten.texifyidea.util.parser.getIdentifier
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * @author Hannah Schellekens
 */
open class BibtexDuplicateIdInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.BIBTEX

    override fun getDisplayName() = "Duplicate ID"

    override val inspectionId = "DuplicateId"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        // Contains all the \bibitem commands in the file set.
        val bibitems = file.commandsInFileSet().asSequence().findLatexCommandsLabels(file.project)
            .filter { it.name == "\\bibitem" }
            .mapNotNull { it.requiredParameter(0) }
            .toSet()

        // All the ids that have been defined in the bibtex file. And next a list of all names.
        val bibtexIds = BibtexEntryIndex().getIndexedEntriesInFileSet(file)
        val strings = bibtexIds.map { it.getIdentifier() }.toList()

        val added = HashSet<BibtexEntry>()
        // Check the bibtexIds in the current file
        for (bibtexEntry in BibtexEntryIndex().getIndexedEntries(file)) {
            val idName = bibtexEntry.getIdentifier()

            // Check if defined as bibitem.
            if (bibtexEntry !in added && idName in bibitems) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        bibtexEntry,
                        TextRange(0, bibtexEntry.textLength - 1),
                        "Duplicate identifier '$idName'",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
                added += bibtexEntry
                continue
            }

            // Check if defined in bibtex files.
            if (bibtexEntry !in added && strings.findAtLeast(2) { it == idName }) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        bibtexEntry,
                        TextRange(0, bibtexEntry.textLength - 1),
                        "Duplicate identifier '$idName'",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
                added += bibtexEntry
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.bibtex

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.openedTextEditor
import nl.hannahsten.texifyidea.util.lineIndentationByOffset

/**
 * @author Hannah Schellekens
 */
open class BibtexMissingBibliographystyleInspection : TexifyInspectionBase() {

    // Manual override to match short name in plugin.xml
    override fun getShortName() = InsightGroup.BIBTEX.prefix + inspectionId

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "MissingBibliographystyle"

    override fun getDisplayName() = "Missing bibliography style"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.commandsInFileSet()
        val bibCmd = commands.find { it.containingFile == file && it.name == "\\bibliography" } ?: return descriptors
        if (commands.none { it.name == "\\bibliographystyle" }) {
            descriptors.add(
                manager.createProblemDescriptor(
                    bibCmd,
                    TextRange(0, bibCmd.commandToken.textLength),
                    "No \\bibliographystyle defined",
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly,
                    InsertStyleFix
                )
            )
        }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    object InsertStyleFix : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Insert \\bibliographystyle"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val file = command.containingFile
            val document = file.document() ?: return
            val editor = file.openedTextEditor() ?: return

            val offset = command.endOffset()
            val indent = document.lineIndentationByOffset(offset)
            val string = "\n$indent\\bibliographystyle{}"
            document.insertString(offset, string)
            editor.caretModel.moveToOffset(offset + string.length - 1)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.bibtex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.refactoring.suggested.createSmartPointer
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.SafeDeleteFix
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.BibtexId
import nl.hannahsten.texifyidea.util.parser.childrenOfType

class BibtexUnusedEntryInspection : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup = InsightGroup.BIBTEX

    override val inspectionId: String = "UnusedEntry"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> =
        file.childrenOfType(BibtexId::class)
            .asSequence()
            .filter { ReferencesSearch.search(it).toList().isEmpty() }
            .map {
                manager.createProblemDescriptor(
                    it,
                    "Bibtex entry is not used",
                    RemoveBibtexEntryFix(it.createSmartPointer()),
                    ProblemHighlightType.LIKE_UNUSED_SYMBOL,
                    isOntheFly
                )
            }
            .toList()

    class RemoveBibtexEntryFix(private val id: SmartPsiElementPointer<BibtexId>) : SafeDeleteFix(id.element as PsiElement) {

        override fun getText(): String = "Safe delete ${id.element?.text}"
    }
}

package nl.hannahsten.texifyidea.inspections.grazie

import com.intellij.grazie.text.ProblemFilter
import com.intellij.grazie.text.TextProblem

class LatexProblemFilter : ProblemFilter() {

    override fun shouldIgnore(problem: TextProblem): Boolean {
        return false
    }
}

package nl.hannahsten.texifyidea.inspections.grazie

import com.intellij.grazie.grammar.strategy.StrategyUtils
import com.intellij.grazie.text.TextContent
import com.intellij.grazie.text.TextExtractor
import com.intellij.psi.PsiElement
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.lang.commands.Argument
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.merge
import nl.hannahsten.texifyidea.util.overlaps
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.parents
import nl.hannahsten.texifyidea.util.toTextRange

/**
 * Explains to Grazie which psi elements contain text and which don't.
 */
class LatexTextExtractor : TextExtractor() {

    override fun buildTextContent(root: PsiElement, allowedDomains: MutableSet<TextContent.TextDomain>): TextContent? {
        if (root !is LatexContent) {
            // It is important to return null instead of TextContent, otherwise Grazie will not search higher up in the psi tree, and we will not get this function called for non-leaf elements and the above lines will not work
            return null
        }

        // Since Grazie works by first checking leaf elements, and if it gets null tries one level higher, we cannot return anything (e.g. literal for a command, comment for comments) other than LatexContent because then LatexContent itself will not be used as a root.
        // However, we do need it as a root because we need to filter out certain things like inline math ourselves, so that we can make sure all the whitespace around ignored items is correct.
        val domain = TextContent.TextDomain.PLAIN_TEXT

        val textContent = TextContent.builder().build(root, domain) ?: return null
        val stealthyRanges = getStealthyRanges(root)
            // Convert IntRange (inclusive end) to TextRange (exclusive end)
            .map { TextContent.Exclusion.exclude(it.toTextRange()) }
            .filter { it.start >= 0 && it.end <= textContent.length }

        return textContent.excludeRanges(stealthyRanges)
    }

    /**
     * Get ranges to ignore.
     * Note: IntRange has an inclusive end.
     */
    private fun getStealthyRanges(root: PsiElement): List<IntRange> {
        // Getting text takes time, so we only do it once
        val rootText = root.text

        // Only keep normaltext, assuming other things (like inline math) need to be ignored.
        val ranges = (root.childrenOfType(LatexNormalText::class) + root.childrenOfType<LatexParameterText>())
            .asSequence()
            .filter { it.isNotInMathEnvironment() && it.isNotInSquareBrackets() }
            // Ranges that we need to keep
            // Note that textRangeInParent will not be correct because that's the text range in the direct parent, not in the root
            .flatMap { text ->
                // Skip arguments of non-text commands
                if (text is LatexParameterText && LatexCommand.lookup(text.firstParentOfType(LatexCommands::class)?.name)?.firstOrNull()?.arguments?.any { it.type == Argument.Type.TEXT } != true) {
                    return@flatMap emptyList()
                }

                var start = text.textRange.startOffset - root.startOffset
                // If LatexNormalText starts after a newline following a command, the newline is not part of the LatexNormalText so we include it manually to make sure that it is seen as a space between sentences
                // NOTE: it is not allowed to start the text we send to Grazie with a newline! If we do, then Grazie will just not do anything. So we exclude the newline for the first normal text in the file.
                if (setOf(' ', '\n').contains(rootText.getOrNull(start - 1)) && root.childrenOfType(LatexNormalText::class).firstOrNull() != text
                ) {
                    //  We have to skip over indents to find the newline though (indents will be ignored later)
                    start -= rootText.substring(0, start).takeLastWhile { it.isWhitespace() }.length
                }

                // -1 Because endOffset is exclusive, but we are working with inclusive end here
                var end = text.textRange.endOffset - 1 - root.startOffset
                // If LatexNormalText ends, for example because it is followed by a command, we do want to include the space in front of the command, since it is still typeset as a space, which is not true for the space after the command
                // except when the space is followed by inline math, since we ignore inline math altogether (which is probably not correct) we should also ignore the space
                if (setOf(' ', '\n').contains(rootText.getOrNull(end + 1)) && rootText.getOrNull(end + 2) != '$') {
                    end += 1
                }
                listOf(start, end)
            }
            .sorted()
            .toMutableList()
            // Make sure that if the root does not start/end with normal text, that those parts are excluded
            .also { it.add(0, -1) }
            .also { it.add(root.endOffset()) }
            // To get the ranges that we need to ignore
            // + 1 because we want to exclude all letters _after_ the last letter that we want to keep (the ranges defined above are inclusive)
            // -1 because IntRange has inclusive end, but we want to exclude all letters _excluding_ the letter where the normal text started
            .chunked(2) { IntRange(it[0] + 1, it[1] - 1) }
            .filter { it.first <= it.last && it.first >= 0 && it.last < rootText.length }
            .toMutableSet()

        // There is still a bit of a problem, because when stitching together the NormalTexts, whitespace is lost
        // so this leads Grazie to think that there is no space there, while in fact there may or may not be

        // Add indents as ranges to ignore
        // Currently, GrammarChecker does not handle overlapped ranges, so we do that ourselves
        for (indent in StrategyUtils.indentIndexes(root.text, setOf(' '))) {
            val overlapped = ranges.filter { indent.overlaps(it) }
            ranges.removeAll(overlapped.toSet())
            ranges.add(indent.merge(overlapped))
        }
        // This is approximately (except at the start) the text we send to Grazie
//        val text = ranges.sortedBy { it.first }.flatMap { listOf(it.first, it.last) }.toMutableList().also { it.add(0, -1) }
//            .chunked(2) { if (it.size > 1) rootText.substring(it[0] + 1, it[1]) else null }
        return ranges.sortedBy { it.first }
    }

    private fun PsiElement.isNotInMathEnvironment() = parents().none { it is LatexMathEnvironment }

    private fun PsiElement.isNotInSquareBrackets() = parents().find { it is LatexGroup || it is LatexOptionalParam }
        ?.let { it is LatexGroup } ?: true
}

package nl.hannahsten.texifyidea.lang.alias

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.psi.LatexCommands
import java.util.concurrent.ConcurrentHashMap

/**
 * Manage aliases for commands and environments.
 */
abstract class AliasManager {

    /**
     * Maps a command to a set of aliases including the command itself.
     * Similar for environments.
     *
     * All elements of the set are in the map as keys as well linking to the set in which they
     * are themselves. This means that the set of all keys that are aliases of each other all
     * link to the same set of aliases. This ensures that you only have to modify the alias sets
     * once and automatically update for all others.
     *
     *
     * The commands are stored including the backslash.
     *
     *
     * *Example:*
     *
     *
     * Definitions:

     * `A := {\one, \een, \ein}`

     * `B := {\twee}`

     * `C := {\drie, \three}`
     *
     *
     * Map:

     * `\one => A`

     * `\een => A`

     * `\ein => A`

     * `\twee => B`

     * `\three => C`

     * `\drie => C`
     */
    open val aliases = ConcurrentHashMap<String, MutableSet<String>>()

    /**
     * We have to somehow know when we need to look for new aliases.
     * We do this by keeping track of the \newcommand-like commands in the index,
     * and when this changes we go gather new aliases.
     */
    open var indexedCommandDefinitions = setOf<LatexCommands>()

    /**
     * Register a new item, which creates a new alias set.
     */
    fun register(alias: String) {
        require(!isRegistered(alias)) { "alias '$alias' has already been registered" }
        synchronized(aliases) {
            aliases[alias] = mutableSetOf(alias)
        }
    }

    /**
     * Checks if the given alias has been registered to the alias manager.
     *
     * @param alias
     * The alias to check if it has been registered
     * *E.g. `\begin`*
     * @return `true` if the given alias is present in the alias manager, `false`
     * otherwise.
     */
    fun isRegistered(alias: String): Boolean {
        synchronized(aliases) {
            return aliases.containsKey(alias)
        }
    }

    /**
     * Register an alias for a given item.
     *
     *
     * The alias will be added to the set of aliases for the given item.
     *
     * @param item
     * An existing item to register the alias for starting with a backslash. *E.g.
     * `\begin`*
     * @param alias
     * The alias to register for the item. This could be either
     * a new item, or an existing item *E.g. `\start`*
     */
    @Synchronized
    fun registerAlias(item: String, alias: String) {
        synchronized(aliases) {
            val aliasSet = aliases[item] ?: mutableSetOf()

            // If the alias is already assigned: unassign it.
            if (isRegistered(alias)) {
                val previousAliases = aliases[alias]
                previousAliases?.remove(alias)
                aliases.remove(alias)
            }
            aliasSet.add(alias)
            aliases[alias] = aliasSet
        }
    }

    /**
     * Get an unmodifiable set with all the aliases for the given item.
     *
     * @param item
     * An existing item to get all aliases of starting with a backslash. *E.g. `\begin`*
     * @return An unmodifiable set of all aliases including the item itself. Returns the empty set if the item is not registered.
     */
    fun getAliases(item: String): Set<String> {
        if (!isRegistered(item)) return emptySet()
        synchronized(aliases) {
            return aliases[item]?.toSet() ?: emptySet()
        }
    }

    /**
     * If needed (based on the number of indexed \newcommand-like commands) check for new aliases of the given alias set. This alias can be any alias of its alias set.
     * If the alias set is not yet registered, it will be registered as a new alias set.
     */
    @Synchronized
    fun updateAliases(aliasSet: Collection<String>, project: Project) {
        // Register if needed
        if (aliasSet.isEmpty()) return
        val firstAlias = aliasSet.first()
        var wasRegistered = false
        if (!isRegistered(firstAlias)) {
            register(firstAlias)
            wasRegistered = true
            aliasSet.forEach { registerAlias(firstAlias, it) }
        }

        // If the command name itself is not directly in the given set, check if it is perhaps an alias of a command in the set
        // Uses projectScope now, may be improved to filesetscope
        val indexedCommandDefinitions = LatexDefinitionIndex.getItems(project).toSet()

        // Check if something has changed (the number of indexed command might be the same while the content is different), and if so, update the aliases.
        // Also do this the first time something is registered, because then we have to update aliases as well
        val hasNotChanged = this.indexedCommandDefinitions == indexedCommandDefinitions
        if (!hasNotChanged || wasRegistered) {
            // Update everything, since it is difficult to know beforehand what aliases could be added or not
            // Alternatively we could save a numberOfIndexedCommandDefinitions per alias set, and only update the
            // requested alias set (otherwise only the first alias set requesting an update will get it)
            // We have to deepcopy the set of alias sets before iterating over it, because we want to modify aliases
            synchronized(aliases) {
                val deepCopy = aliases.values.map { it1 -> it1.map { it }.toSet() }.toSet()
                for (copiedAliasSet in deepCopy) {
                    findAllAliases(copiedAliasSet, indexedCommandDefinitions)
                }
            }

            this.indexedCommandDefinitions = indexedCommandDefinitions.toSet()
        }
    }

    /**
     * Find all aliases that are defined in the [indexedDefinitions] and register them.
     */
    abstract fun findAllAliases(
        aliasSet: Set<String>,
        indexedDefinitions: Collection<LatexCommands>
    )
}

package nl.hannahsten.texifyidea.lang.alias

import nl.hannahsten.texifyidea.lang.LabelingCommandInformation
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.containsAny
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import nl.hannahsten.texifyidea.util.parser.requiredParameters
import java.io.Serializable
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Stream
import java.util.stream.StreamSupport
import kotlin.collections.set

/**
 * Manages all available LaTeX commands and their aliases.
 *
 *
 * The `CommandManager` differentiates between two types of commands: aliases and original
 * commands.
 *
 *
 * When a command is registered to the manager ([CommandManager.registerCommand]), it
 * is marked as an *original command*. When the original command is changed into an alias for
 * another command, the command manager still makes sure that you can look up the original
 * functionality of the command. This is to cover cases when for example somebody uses `\renewcommand`.
 *
 *
 * *Aliases* are just what they say on the tin: alternate names for a command. The manager
 * makes you register and look up all aliases for a given command.
 *
 * @author Hannah Schellekens
 */
// Currently it is a singleton, in the future this may be one instance per fileset
object CommandManager : Iterable<String?>, Serializable, AliasManager() {

    /**
     * Maps an original command to the set of current aliases.
     *
     *
     * When adding new aliases, it could happen that some commands lose their functionality in
     * favour of another (using for example `\renewcommand`.) This map maps the original
     * commands to the instance of the set of new aliases.
     *
     *
     * The commands are stored including the backslash.
     *
     *
     * *Example:*
     *
     *
     * Latex:

     * `\let\goodepsilon\varepsilon`

     * `\let\varepsilon\epsilon`

     * `\let\epsilon\goodepsilon`
     *
     *
     * Definitions:

     * `A := {\varepsilon, \goodepsilon}`

     * `B := {\epsilon}`
     *
     *
     * Map:

     * `\epsilon => A`

     * `\varepsilon => B`
     */
    private val original = ConcurrentHashMap<String, Set<String>>()

    /**
     * Map commands that define labels to the positions of the label parameter.
     * For example, in \newcommand{\mylabel}[2]{\section{#1}\label{sec:#2} the label parameter is second, so \mylabel maps to [2].
     * It is not so nice to have to maintain this separately, but maintaining
     * parameter position mappings between general alias sets is too much overhead for now.
     */
    val labelAliasesInfo: MutableMap<String, LabelingCommandInformation> =
        CommandMagic.labelDefinitionsWithoutCustomCommands.associateWith {
            LabelingCommandInformation(
                listOf(0),
                true
            )
        }.toMutableMap()

    /**
     * Registers a brand new command to the command manager.
     *
     *
     * The command you register must be unique and will become an original command.
     *
     * @param command
     * A new command (should not exist already) starting with a backslash. *E.g. `\begin`*
     * @throws IllegalArgumentException
     * When the command has already been registered.s
     */
    @Throws(IllegalArgumentException::class)
    fun registerCommand(command: String) {
        super.register(command)
        original[command] = aliases[command] ?: mutableSetOf(command)
    }

    /**
     * Registers a brand new command to the command manager.
     *
     *
     * The command you register must be unique and will become an original command.
     *
     * @param commandNoSlash
     * A new command (should not exist already) starting without the command backslash. *E
     * .g. `begin`*
     * @throws IllegalArgumentException
     * When the command has already been registered.s
     */
    @Throws(IllegalArgumentException::class)
    fun registerCommandNoSlash(commandNoSlash: String) {
        registerCommand("\\" + commandNoSlash)
    }

    /**
     * Register an alias for a given command.
     *
     *
     * The alias will be added to the set of aliases for the given command. The alias will be
     * removed from its original alias set if the alias is an existing command.
     *
     * @param commandNoSlash
     * An existing command to register the alias for starting without the command backslash.
     * *E.g. `begin`*
     * @param aliasNoSlash
     * The alias to register for the command starting without the command backslash. This
     * could be either a new command, or an existing command *E.g. `start`*
     * @throws IllegalArgumentException
     * When the given command already exixts.
     */
    @Throws(IllegalArgumentException::class)
    fun registerAliasNoSlash(commandNoSlash: String, aliasNoSlash: String) {
        registerAlias("\\" + commandNoSlash, "\\" + aliasNoSlash)
    }

    override fun findAllAliases(
        aliasSet: Set<String>,
        indexedDefinitions: Collection<LatexCommands>
    ) {
        val firstAlias = aliasSet.first()

        // Get definitions which define one of the commands in the given command names set
        // These will be aliases of the given set (which is assumed to be an alias set itself)
        indexedDefinitions.filter {
            // Assume the parameter definition has the command being defined in the first required parameter,
            // and the command definition itself in the second
            it.requiredParameter(1)?.containsAny(aliasSet) == true
        }
            .mapNotNull { it.requiredParameter(0) }
            .forEach { registerAlias(firstAlias, it) }

        // Extract label parameter positions
        // Assumes the predefined label definitions all have the label parameter in the same position
        // For example, in \newcommand{\mylabel}[2]{\section{#1}\label{sec:#2}} we want to parse out the 2 in #2
        if (aliasSet.intersect(CommandMagic.labelDefinitionsWithoutCustomCommands).isNotEmpty()) {
            indexedDefinitions.forEach { commandDefinition ->
                val definedCommand = commandDefinition.requiredParameter(0) ?: return@forEach
                if (definedCommand.isBlank()) return@forEach

                val isFirstParameterOptional = commandDefinition.parameterList.filter { it.optionalParam != null }.size > 1

                val parameterCommands = commandDefinition.requiredParameters().getOrNull(1)
                    ?.requiredParamContentList
                    ?.flatMap { it.childrenOfType(LatexCommands::class) }
                    ?.asSequence()

                // Positions of label parameters in the custom commands (starting from 0)
                val positions = parameterCommands
                    ?.filter { it.name in CommandMagic.labelDefinitionsWithoutCustomCommands }
                    ?.mapNotNull { it.requiredParameter(0) }
                    ?.mapNotNull {
                        if (it.indexOf('#') != -1) {
                            it.getOrNull(it.indexOf('#') + 1)
                        }
                        else null
                    }
                    ?.map(Character::getNumericValue)
                    // LaTeX starts from 1, we from 0 (consistent with how we count required parameters)
                    ?.map { it - 1 }
                    // For the moment we only consider required parameters and ignore the optional one
                    ?.map { if (isFirstParameterOptional) it - 1 else it }
                    ?.filter { it >= 0 }
                    ?.toList() ?: return@forEach
                if (positions.isEmpty()) return@forEach

                // Check if there is a command which increases a counter before the \label
                // If so, the \label just labels the counter increasing command, and not whatever will appear before usages of the custom labeling command
                val definitionContainsIncreaseCounterCommand =
                    parameterCommands.takeWhile { it.name !in CommandMagic.labelDefinitionsWithoutCustomCommands }
                        .any { it.name in CommandMagic.increasesCounter }

                val prefix = parameterCommands.filter { it.name in CommandMagic.labelDefinitionsWithoutCustomCommands }
                    .mapNotNull { it.requiredParameter(0) }
                    .map {
                        if (it.indexOf('#') != -1) {
                            val prefix = it.substring(0, it.indexOf('#'))
                            prefix.ifBlank { "" }
                        }
                        else ""
                    }.firstOrNull() ?: ""

                labelAliasesInfo[definedCommand] =
                    LabelingCommandInformation(positions, !definitionContainsIncreaseCounterCommand, prefix)
            }
        }
    }

    /**
     * Get an unmodifiable set with all the aliases for the given command.
     *
     * @param commandNoSlash
     * An existing command to get all aliases of starting without the command backslash.
     * *E.g. `begin`*
     * @return An unmodifiable set of all aliases including the command itself. All aliases include
     * a command backslash.
     * @throws IllegalArgumentException
     * When the given command is not registered.
     */
    @Throws(IllegalArgumentException::class)
    fun getAliasesNoSlash(commandNoSlash: String): Set<String> {
        return getAliases("\\" + commandNoSlash)
    }

    /**
     * Get an unmodifiable set with all the aliases for an original command.
     *
     *
     * A command is original when it first gets registered to the CommandManager. This way you
     * can get all updated aliases in case the given command no longer provides the original
     * functionality.
     *
     * @param originalCommand
     * The original command of which to get the aliases of starting with the command
     * backslash. *E.g. `\begin`*
     * @return An unmodifiable set of all aliases of the original command. All aliases include a
     * command backslash.
     * @throws IllegalArgumentException
     * When the original command has not been registered.
     */
    @Throws(IllegalArgumentException::class)
    fun getAliasesFromOriginal(originalCommand: String): Set<String> {
        require(isOriginal(originalCommand)) { "originalCommand '$originalCommand' has not been registered" }
        return original[originalCommand]?.toSet() ?: emptySet()
    }

    /**
     * Get an unmodifiable set with all the aliases for an original command.
     *
     *
     * A command is original when it first gets registered to the CommandManager. This way you
     * can get all updated aliases in case the given command no longer provides the original
     * functionality.
     *
     * @param originalCommandNoSlash
     * The original command of which to get the aliases of starting without the command
     * backslash. *E.g. `begin`*
     * @return An unmodifiable set of all aliases of the original command. All aliases include a
     * command backslash.
     * @throws IllegalArgumentException
     * When the original command has not been registered.
     */
    @Throws(IllegalArgumentException::class)
    fun getAliasesFromOriginalNoSlash(originalCommandNoSlash: String): Set<String> {
        return getAliasesFromOriginal("\\" + originalCommandNoSlash)
    }

    /**
     * Get an unmodifiable set of all registered commands (including aliases).
     *
     * @return An unmodifiable set of all commands. All command include a command backslash.
     */
    val allCommands: Set<String>
        get() = Collections.unmodifiableSet(aliases.keys)

    /**
     * Checks if the given command has been registered to the command manager.
     *
     * @param commandNoSlash
     * The command to check if it has been registered starting without the command
     * backslash. *E.g. `begin`*
     * @return `true` if the given command is present in the command manager, `false`
     * otherwise.
     */
    fun isRegisteredNoSlash(commandNoSlash: String): Boolean {
        return isRegistered("\\" + commandNoSlash)
    }

    /**
     * Checks if the given command is an original command.
     *
     *
     * A command is original when it gets registered to the command manager instead of being set
     * as alias.
     *
     * @param command
     * The command to check if it is an original command starting with the command
     * backslash. *E.g. `\begin`*
     * @return `true` if the given command is an original command in the command manager,
     * `false` if not.
     */
    fun isOriginal(command: String): Boolean {
        return original.containsKey(command)
    }

    /**
     * Checks if the given command is an original command.
     *
     *
     * A command is original when it gets registered to the command manager instead of being set
     * as alias.
     *
     * @param commandNoSlash
     * The command to check if it is an original command starting without the command
     * backslash. *E.g. `begin`*
     * @return `true` if the given command is an original command in the command manager,
     * `false` if not.
     */
    fun isOriginalNoSlash(commandNoSlash: String): Boolean {
        return isOriginal("\\" + commandNoSlash)
    }

    /**
     * Removes all commands from the command manager.
     */
    fun clear() {
        aliases.clear()
        original.clear()
    }

    /**
     * Gets the amount of registered aliases.
     */
    fun size(): Int {
        return aliases.size
    }

    /**
     * Gets the amount of original commands.
     *
     *
     * A command is original when it gets registered to the command manager instead of being set
     * as alias.
     */
    fun originalSize(): Int {
        return original.size
    }

    /**
     * Create a stream of aliases.
     */
    fun stream(): Stream<String?> {
        return StreamSupport.stream(spliterator(), false)
    }

    /**
     * Create a parallel stream of aliases.
     */
    fun parallelStream(): Stream<String?> {
        return StreamSupport.stream(spliterator(), true)
    }

    /**
     * Create a stream of original commands.
     *
     *
     * A command is original when it gets registered to the command manager instead of being set
     * as alias.
     */
    fun streamOriginal(): Stream<String> {
        return StreamSupport.stream(spliteratorOriginal(), false)
    }

    /**
     * Create a parallel stream of original commands.
     *
     *
     * A command is original when it gets registered to the command manager instead of being set
     * as alias.
     */
    fun parallelStreamOriginal(): Stream<String> {
        return StreamSupport.stream(spliteratorOriginal(), true)
    }

    /**
     * Iterator for aliases.
     */
    override fun iterator(): MutableIterator<String> {
        return aliases.keys.iterator()
    }

    /**
     * Spliterator for aliases.
     */
    override fun spliterator(): Spliterator<String?> {
        return aliases.keys.spliterator()
    }

    /**
     * Iterator for original commands.
     *
     *
     * A command is original when it gets registered to the command manager instead of being set
     * as alias.
     */
    fun iteratorOriginal(): Iterator<String> {
        return original.keys.iterator()
    }

    /**
     * Spliterator for original commands.
     *
     *
     * A command is original when it gets registered to the command manager instead of being set
     * as alias.
     */
    private fun spliteratorOriginal(): Spliterator<String> {
        return original.keys.spliterator()
    }

    override fun toString(): String {
        return "CommandManager{" + "aliases=" + aliases +
            ", original=" + original +
            '}'
    }
}

package nl.hannahsten.texifyidea.lang.alias

import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.containsAny
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * Similar to the [CommandManager], this manages aliases of environments.
 */
object EnvironmentManager : AliasManager() {

    override fun findAllAliases(aliasSet: Set<String>, indexedDefinitions: Collection<LatexCommands>) {
        val firstAlias = aliasSet.first()

        // Assume the environment that is defined is the first parameter, and that the first part of the definition is in the second
        // e.g. \newenvironment{mytabl}{\begin{tabular}{cc}}{\end{tabular}}
        indexedDefinitions.filter { definition ->
            definition.requiredParameter(1)?.containsAny(aliasSet.map { "\\begin{$it}" }.toSet()) == true
        }
            .mapNotNull { it.requiredParameter(0) }
            .forEach { registerAlias(firstAlias, it) }
    }
}

package nl.hannahsten.texifyidea.lang.commands

import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import nl.hannahsten.texifyidea.completion.LatexBibliographyStyleProvider
import nl.hannahsten.texifyidea.completion.LatexListTypeProvider

/**
 * @author Sten Wessel
 */
abstract class Argument @JvmOverloads protected constructor(val name: String, val type: Type = Type.NORMAL) {

    abstract override fun toString(): String

    /**
     * @author Hannah Schellekens, Sten Wessel
     */
    enum class Type(

        /**
         * Provides the autocomplete options for the argument of this type.
         * `null` for no autocomplete options.
         */
        val completionProvider: CompletionProvider<CompletionParameters>? = null
    ) {

        /**
         * Can contain any kind of argument content.
         */
        NORMAL,

        /**
         * Contains a path/reference to a file.
         */
        FILE,

        /**
         * Reference to a label.
         */
        LABEL,

        /**
         * Text contents.
         */
        TEXT,

        /**
         * Contains a bibliography style.
         */
        BIBLIOGRAPHY_STYLE(completionProvider = LatexBibliographyStyleProvider),

        /**
         * enumerate, itemize, etc.
         */
        LIST_ENVIRONMENT(completionProvider = LatexListTypeProvider),
    }
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.ALGPSEUDOCODE

/**
 * @author Hannah Schellekens
 */
enum class LatexAlgorithmicxCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    FOR("For", "condition".asRequired(), dependency = ALGPSEUDOCODE),
    FORALL("ForAll", "condition".asRequired(), dependency = ALGPSEUDOCODE),
    ENDFOR("EndFor", dependency = ALGPSEUDOCODE),
    IF_ALGPSEUDOCODE("If", "condition".asRequired(), dependency = ALGPSEUDOCODE),
    ELSIF("ElsIf", "condition".asRequired(), dependency = ALGPSEUDOCODE),
    ENDIF("EndIf", dependency = ALGPSEUDOCODE),
    WHILE("While", "condition".asRequired(), dependency = ALGPSEUDOCODE),
    ENDWHILE("EndWhile", dependency = ALGPSEUDOCODE),
    REPEAT("Repeat", dependency = ALGPSEUDOCODE),
    UNTIL("Until", "condition".asRequired(), dependency = ALGPSEUDOCODE),
    LOOP("Loop", dependency = ALGPSEUDOCODE),
    ENDLOOP("EndLoop", dependency = ALGPSEUDOCODE),
    FUNCTION("Function", "name".asRequired(), "params".asRequired(), dependency = ALGPSEUDOCODE),
    ENDFUNCTION("EndFunction", dependency = ALGPSEUDOCODE),
    PROCEDURE("Procedure", "name".asRequired(), "params".asRequired(), dependency = ALGPSEUDOCODE),
    ENDPROCEDURE("EndProcedure", dependency = ALGPSEUDOCODE),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSSYMB
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.LATEXSYMB
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.STMARYRD

/**
 * @author Hannah Schellekens
 */
enum class LatexArrowCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = true,
    val collapse: Boolean = false
) : LatexCommand {

    NOT_RIGHT_ARROW("nrightarrow", dependency = AMSSYMB, display = "↛", collapse = true),
    LEFT_ARROW("leftarrow", display = "←", collapse = true),
    LEFT_DOUBLE_ARROW("Leftarrow", display = "⇐", collapse = true),
    RIGHT_ARROW("rightarrow", display = "→", collapse = true),
    RIGHT_DOUBLE_ARROW("Rightarrow", display = "⇒", collapse = true),
    LEFT_AND_RIGHT_ARROW("leftrightarrow", display = "↔", collapse = true),
    LEFT_AND_RIGHT_DOUBLE_ARROW("Leftrightarrow", display = "⇔", collapse = true),
    LONG_LEFT_ARROW("longleftarrow", display = "⟵", collapse = true),
    LONG_LEFT_DOUBLE_ARROW("Longleftarrow", display = "⟸", collapse = true),
    LONG_RIGHT_ARROW("longrightarrow", display = "⟶", collapse = true),
    L_ONGRIGHTARROW("Longrightarrow", display = "⟹", collapse = true),
    LONG_LEFT_AND_RIGHT_ARROW("longleftrightarrow", display = "⟷", collapse = true),
    LONG_LEFT_AND_RIGHT_DOUBLE_ARROW("Longleftrightarrow", display = "⟺", collapse = true),
    UP_ARROW("uparrow", display = "↑", collapse = true),
    UP_DOUBLE_ARROW("Uparrow", display = "⇑", collapse = true),
    DOWN_ARROW("downarrow", display = "↓", collapse = true),
    DOWN_DOUBLE_ARROW("Downarrow", display = "⇓", collapse = true),
    UP_AND_DOWN_ARROW("updownarrow", display = "↕", collapse = true),
    UP_AND_DOWN_DOUBLE_ARROW("Updownarrow", display = "⇕", collapse = true),
    MAPS_TO("mapsto", display = "↦", collapse = true),
    LEFT_ARROW_WITH_HOOK("hookleftarrow", display = "↩", collapse = true),
    LEFT_HARPOON_UP("leftharpoonup", display = "↼", collapse = true),
    LEFT_HARPOON_DOWN("leftharpoondown", display = "↽", collapse = true),
    RIGHT_AND_LEFT_HARPOONS("rightleftharpoons", display = "⇌", collapse = true),
    LONG_MAPS_TO("longmapsto", display = "⟼", collapse = true),
    RIGHT_ARROW_WITH_HOOK("hookrightarrow", display = "↪", collapse = true),
    RIGHT_HARPOON_UP("rightharpoonup", display = "⇀", collapse = true),
    RIGHT_HARPOON_DOWN("rightharpoondown", display = "⇁", collapse = true),
    LEADSTO("leadsto", dependency = LATEXSYMB, display = "⤳", collapse = true),
    NORTH_EAST_ARROW("nearrow", display = "↗", collapse = true),
    SOUTH_EAST_ARROW("searrow", display = "↘", collapse = true),
    SOUTH_WEST_ARROW("swarrow", display = "↙", collapse = true),
    NORTH_WEST_ARROW("nwarrow", display = "↖", collapse = true),
    DASHED_RIGHT_ARROW("dashrightarrow", dependency = AMSSYMB, display = "⤍", collapse = true),
    LEFT_AND_RIGHT_ARROW_PAIR("leftrightarrows", dependency = AMSSYMB, display = "⇆", collapse = true),
    LEFT_ARROW_WITH_TAIL("leftarrowtail", dependency = AMSSYMB, display = "↢", collapse = true),
    ANTI_CLOCKWISE_SEMICIRCLE_ARROW_TOP("curvearrowleft", dependency = AMSSYMB, display = "↶", collapse = true),
    UP_ARROW_PAIR("upuparrows", dependency = AMSSYMB, display = "⇈", collapse = true),
    MULTIMAP("multimap", dependency = AMSSYMB, display = "⊸", collapse = true),
    RIGHT_AND_LEFT_ARROW_PAIR("rightleftarrows", dependency = AMSSYMB, display = "⇄", collapse = true),
    TWO_HEADED_RIGHT_ARROW("twoheadrightarrow", dependency = AMSSYMB, display = "↠", collapse = true),
    UP_ARROW_WITH_RIGHT_TIP("Rsh", dependency = AMSSYMB, display = "↱", collapse = true),
    DOWN_HARPOON_RIGHT("downharpoonright", dependency = AMSSYMB, display = "⇂", collapse = true),
    DASHED_LEFT_ARROW("dashleftarrow", dependency = AMSSYMB, display = "⇠", collapse = true),
    LEFT_TRIPLE_ARROW("Lleftarrow", dependency = AMSSYMB, display = "⇚", collapse = true),
    LEFT_ARROW_WITH_LOOP("looparrowleft", dependency = AMSSYMB, display = "↫", collapse = true),
    ANTI_CLOCKWISE_CIRCLE_ARROW("circlearrowleft", dependency = AMSSYMB, display = "↺", collapse = true),
    UP_HARPOON_LEFT("upharpoonleft", dependency = AMSSYMB, display = "↿", collapse = true),
    LEFT_AND_RIGHT_WAVE_ARROW("leftrightsquigarrow", dependency = AMSSYMB, display = "↭", collapse = true),
    RIGHT_ARROW_PAIR("rightrightarrows", dependency = AMSSYMB, display = "⇉", collapse = true),
    RIGHT_ARROW_WITH_TAIL("rightarrowtail", dependency = AMSSYMB, display = "↣", collapse = true),
    CLOCKWISE_SEMICIRCLE_ARROW_TOP("curvearrowright", dependency = AMSSYMB, display = "↷", collapse = true),
    DOWN_ARROW_PAIR("downdownarrows", dependency = AMSSYMB, display = "⇊", collapse = true),
    RIGHT_WAVE_ARROW("rightsquigarrow", dependency = AMSSYMB, display = "⇝", collapse = true),
    LEFT_ARROW_PAIR("leftleftarrows", dependency = AMSSYMB, display = "⇇", collapse = true),
    TWO_HEADED_LEFT_ARROW("twoheadleftarrow", dependency = AMSSYMB, display = "↞", collapse = true),
    LEFT_AND_RIGHT_HARPOONS("leftrightharpoons", dependency = AMSSYMB, display = "↰", collapse = true),
    UP_ARROW_WITH_LEFT_TIP("Lsh", dependency = AMSSYMB, display = "↰", collapse = true),
    DOWN_HARPOON_LEFT("downharpoonleft", dependency = AMSSYMB, display = "⇃", collapse = true),
    RIGHT_ARROW_WITH_LOOP("looparrowright", dependency = AMSSYMB, display = "↬", collapse = true),
    CLOCKWISE_CIRCLE_ARROW("circlearrowright", dependency = AMSSYMB, display = "↻", collapse = true),
    UP_HARPOON_RIGHT("upharpoonright", dependency = AMSSYMB, display = "↾", collapse = true),
    NOT_LEFT_ARROW("nleftarrow", dependency = AMSSYMB, display = "↚", collapse = true),
    NOT_LEFT_DOUBLE_ARROW("nLeftarrow", dependency = AMSSYMB, display = "⇍", collapse = true),
    NOT_RIGHT_DOUBLE_ARROW("nRightarrow", dependency = AMSSYMB, display = "⇏", collapse = true),
    NOT_LEFT_AND_RIGHT_ARROW("nleftrightarrow", dependency = AMSSYMB, display = "↮", collapse = true),
    NOT_LEFT_AND_RIGHT_DOUBLE_ARROW("nLeftrightarrow", dependency = AMSSYMB, display = "⇎", collapse = true),
    LONG_MAPS_FROM_DOUBLE_ARROW("Longmapsfrom", dependency = STMARYRD, display = "⟽", collapse = true),
    LONG_MAPS_TO_DOUBLE_ARROW("Longmapsto", dependency = STMARYRD, display = "⟾", collapse = true),
    MAPS_FROM_DOUBLE_ARROW("Mapsfrom", dependency = STMARYRD, display = "⤆", collapse = true),
    MAPS_TO_DOUBLE_ARROW("Mapsto", dependency = STMARYRD, display = "⤇", collapse = true),
    LEFT_OPEN_HEADED_ARROW("leftarrowtriangle", dependency = STMARYRD, display = "⇽", collapse = true),
    RIGHT_OPEN_HEADED_ARROW("rightarrowtriangle", dependency = STMARYRD, display = "⇾", collapse = true),
    LEFT_AND_RIGHT_ARROW_EQUALS("leftrightarroweq", dependency = STMARYRD),
    LEFT_AND_RIGHT_OPEN_HEADED_ARROW("leftrightarrowtriangle", dependency = STMARYRD, display = "⇿", collapse = true),
    LIGHTNING("lightning", dependency = STMARYRD, display = "☇", collapse = true),
    LONG_MAPS_FROM("longmapsfrom", dependency = STMARYRD, display = "⟻", collapse = true),
    MAPS_FROM("mapsfrom", dependency = STMARYRD, display = "↤", collapse = true),
    NORTH_NORTH_EAST_ARROW("nnearrow", dependency = STMARYRD, display = "↗", collapse = true),
    NORTH_NORTH_WEST_ARROW("nnwarrow", dependency = STMARYRD, display = "↖", collapse = true),
    SOUNT_SOUTH_EAST_ARROW("ssearrow", dependency = STMARYRD, display = "↘", collapse = true),
    SOUTH_SOUTH_WEST_ARROW("sswarrow", dependency = STMARYRD, display = "↙", collapse = true),
    SHORT_LEFT_ARROW("shortleftarrow", dependency = STMARYRD, display = "←", collapse = true),
    SHORT_UP_ARROW("shortuparrow", dependency = STMARYRD, display = "↑", collapse = true),
    SHORT_RIGHT_ARROW("shortrightarrow", dependency = STMARYRD, display = "→", collapse = true),
    SHORT_DOWN_ARROW("shortdownarrow", dependency = STMARYRD, display = "↓", collapse = true),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.BIBLATEX

/**
 * @author Hannah Schellekens
 */
enum class LatexBiblatexCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    CITE_CAPITALIZED("Cite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    PARENCITE("parencite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    PARENCITE_CAPITALIZED("Parencite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    FOOTCITE("footcite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    FOOTCITETEXT("footcitetext", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    TEXTCITE("textcite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    TEXTCITE_CAPITALIZED("Textcite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    SMARTCITE("smartcite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    SMARTCITE_CAPITALIZED("Smartcite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    CITE_STAR("cite*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    PARENCITE_STAR("parencite*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    SUPERCITE("supercite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    AUTOCITE("autocite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    AUTOCITE_CAPITALIZED("Autocite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    AUTOCITE_STAR("autocite*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    AUTOCITE_STAR_CAPITALIZED("Autocite*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_CITEAUTHOR("citeauthor", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_CITEAUTHOR_STAR("citeauthor*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_CITEAUTHOR_CAPITALIZED("Citeauthor", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_CITEAUTHOR_STAR_CAPITALIZED("Citeauthor*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_CITETITLE("citetitle", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_CITETITLE_STAR("citetitle*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_CITEYEAR("citeyear", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_CITEYEAR_STAR("citeyear*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    CITEDATE("citedate", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    CITEDATE_STAR("citedate*", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    CITEURL("citeurl", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    VOLCITE("volcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    VOLCITE_CAPITALIZED("Volcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    PVOLCITE("pvolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    PVOLCITE_CAPITALIZED("Pvolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    FVOLCITE("fvolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    FVOLCITE_CAPITALIZED("Fvolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    FTVOLCITE("ftvolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    SVOLCITE("svolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    SVOLCITE_CAPITALIZED("Svolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    TVOLCITE("tvolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    TVOLCITE_CAPITALIZED("Tvolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    AVOLCITE("avolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    AVOLCITE_CAPITALIZED("Avolcite", "prenote".asOptional(), "volume".asRequired(), "page".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    FULLCITE("fullcite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    FOOTFULLCITE("footfullcite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    BIBLATEX_NOCITE("nocite", "key".asRequired(), dependency = BIBLATEX),
    NOTECITE("notecite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    NOTECITE_CAPITALIZED("Notecite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    PNOTECITE("pnotecite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    PNOTECITE_CAPITALIZED("Pnotecite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    FNOTECITE("fnotecite", "prenote".asOptional(), "postnote".asOptional(), "key".asRequired(), dependency = BIBLATEX),
    PARENTTEXT("parenttext", "text".asRequired(Argument.Type.TEXT), dependency = BIBLATEX),
    BRACKETTEXT("brackettext", "text".asRequired(Argument.Type.TEXT), dependency = BIBLATEX),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.MATHTOOLS

/**
 * @author Hannah Schellekens
 */
enum class LatexColoneqCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = true,
    val collapse: Boolean = false
) : LatexCommand {

    COLON_EQUALSS("coloneqq", dependency = MATHTOOLS, display = ":=", collapse = true),
    EQUALSS_COLON("eqqcolon", dependency = MATHTOOLS, display = "=:", collapse = true),
    COLON_EQUALS("coloneq", dependency = MATHTOOLS, display = ":–", collapse = true),
    EQUALS_COLON("eqcolon", dependency = MATHTOOLS, display = "–:", collapse = true),
    COLON_APPROX("colonapprox", dependency = MATHTOOLS, display = ":≈", collapse = true),
    COLON_SIM("colonsim", dependency = MATHTOOLS, display = ":∼", collapse = true),
    DOUBLE_COLON("dblcolon", dependency = MATHTOOLS, display = "::", collapse = true),
    DOUBLE_COLON_EQUALSS("Coloneqq", dependency = MATHTOOLS, display = "::=", collapse = true),
    EQUALSS_DOUBLE_COLON("Eqqcolon", dependency = MATHTOOLS, display = "=::", collapse = true),
    DOUBLE_COLON_EQUALS("Coloneq", dependency = MATHTOOLS, display = "::–", collapse = true),
    EQUALS_DOUBLE_COLON("Eqcolon", dependency = MATHTOOLS, display = "–::", collapse = true),
    DOUBLE_COLON_APPROX("Colonapprox", dependency = MATHTOOLS, display = "::≈", collapse = true),
    DOUBLE_COLON_SIM("Colonsim", dependency = MATHTOOLS, display = "::∼", collapse = true),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.COLOR
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.XCOLOR

/**
 * @author Hannah Schellekens
 */
enum class LatexColorDefinitionCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    DEFINECOLOR("definecolor", "type".asOptional(), "name".asRequired(), "model-list".asRequired(), "spec-list".asRequired(), dependency = COLOR),
    PROVIDECOLOR("providecolor", "type".asOptional(), "name".asRequired(), "model-list".asRequired(), "spec-list".asRequired(), dependency = XCOLOR),
    COLORLET("colorlet", "type".asOptional(), "name".asRequired(), "num model".asOptional(), "color".asRequired(), dependency = XCOLOR),
    DEFINECOLORSET("definecolorset", "type".asOptional(), "model-list".asRequired(), "head".asRequired(), "tail".asRequired(), "set spec".asRequired(), dependency = XCOLOR),
    PROVIDECOLORSET("providecolorset", "type".asOptional(), "model-list".asRequired(), "head".asRequired(), "tail".asRequired(), "set spec".asRequired(), dependency = XCOLOR),
    PREPARECOLOR("preparecolor", "type".asOptional(), "name".asRequired(), "model-list".asRequired(), "spec-list".asRequired(), dependency = XCOLOR),
    PREPARECOLORSET("preparecolorset", "type".asOptional(), "model-list".asRequired(), "head".asRequired(), "tail".asRequired(), "set spec".asRequired(), dependency = XCOLOR),
    DEFINE_NAMED_COLOR("DefineNamedColor", "type".asRequired(), "name".asRequired(), "model-list".asRequired(), "spec-list".asRequired(), dependency = COLOR),
    DEFINECOLORS("definecolors", "id-list".asRequired(), dependency = XCOLOR),
    PROVIDECOLORS("providecolors", "id-list".asRequired(), dependency = XCOLOR),
    DEFINECOLORSERIES("definecolorseries", "name".asRequired(), "core model".asRequired(), "method".asRequired(), "b-model".asOptional(), "b-spec".asRequired(), "s-model".asRequired(), "s-spec".asRequired(), dependency = XCOLOR),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import arrow.core.NonEmptySet
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import kotlinx.coroutines.runBlocking
import nl.hannahsten.texifyidea.index.file.LatexExternalCommandIndex
import nl.hannahsten.texifyidea.lang.Dependend
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.startsWithAny
import kotlin.reflect.KClass

/**
 * @author Hannah Schellekens, Sten Wessel
 */
interface LatexCommand : Described, Dependend {

    companion object {

        /**
         * Looks up the given command name in all predefined [LatexCommand]s.
         *
         * @param commandName
         *          The command name to look up. Can start with or without `\`
         * @return The found commands, or `null` when the command doesn't exist.
         */
        fun lookup(commandName: String?): NonEmptySet<LatexCommand>? {
            var result = commandName ?: return null
            if (result.startsWith("\\")) {
                result = result.substring(1)
            }

            return LatexMathCommand[result] ?: LatexRegularCommand[result]
        }

        /**
         * Create a [LatexCommand] for the given command name, or merge with existing one.
         */
        fun lookupInIndex(cmdWithoutSlash: String, project: Project): Set<LatexCommand> {
            // Don't try to access index when in dumb mode
            if (DumbService.isDumb(project)) return emptySet()
            val cmdWithSlash = "\\$cmdWithoutSlash"

            // Make sure to look up the hardcoded commands, to have something in case nothing is found in the index
            val cmds = lookup(cmdWithSlash)?.toMutableSet() ?: mutableSetOf()

            // Look up in index
            FileBasedIndex.getInstance().processValues(
                LatexExternalCommandIndex.id, cmdWithSlash, null, { file, value ->
                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
                val defaultcmds = lookup(cmdWithSlash)?.filter { it.dependency == dependency } ?: emptyList()
                val cmd = if (defaultcmds.isNotEmpty()) {
                    val defaultCommand = defaultcmds.first()
                    object : LatexCommand {
                        override val command = cmdWithoutSlash
                        override val display = defaultCommand.display
                        override val arguments = defaultCommand.arguments
                        override val description = format(value)
                        override val dependency = dependency
                        override val isMathMode = defaultCommand.isMathMode
                    }
                }
                else {
                    object : LatexCommand {
                        override val command = cmdWithoutSlash
                        override val display: String? = null
                        override val arguments = extractArgumentsFromDocs(value, commandWithSlash)
                        override val description = format(value)
                        override val dependency = dependency
                        override val isMathMode = false
                    }
                }
                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
            // Since an interface cannot override equals, and it has to be an interface because enums implement it, filter duplicates first
            return cmds.distinctBy { listOf(it.command, it.dependency, it.isMathMode, it.description).plus(it.arguments) }
                // Do not group by arguments, because if there is a difference in arguments, probably the command with better (non-empty) docs is also the one with better argument info
                .groupBy { listOf(it.command, it.dependency, it.isMathMode) }
                // Assume empty descriptions appear first when sorted
                .mapValues { it.value.maxByOrNull { cmd -> cmd.description }!! }
                .values.toSet()
        }

        /**
         * Do the last bit of formatting, to remove things that [nl.hannahsten.texifyidea.index.file.LatexDocsRegexer] needed to keep in because we needed the information here.
         */
        fun format(docs: String): String {
            return """^(?:\s*\\[mop]arg\{[^}]+}\s*)*(?:\\\\)?\s*""".toRegex().replace(docs, "")
        }

        /**
         * Parse arguments from docs string, assuming they appear at index [counterInit] (only initial sequence of arguments is considered).
         */
        fun getArgumentsFromStartOfString(docs: String, counterInit: Int): Array<Argument> {
            val arguments = mutableListOf<Argument>()
            var counter = counterInit
            run breaker@{
                // Only use the ones at the beginning of the string to avoid matching too much
                """\s*\\(?<command>[omp]arg)\{(?<arg>.+?)}\s*""".toRegex().findAll(docs, counterInit).forEach {
                    if (it.range.first == counter) {
                        when (it.groups["command"]?.value) {
                            OARG.command -> arguments.add(OptionalArgument(it.groups["arg"]?.value ?: ""))
                            MARG.command -> arguments.add(RequiredArgument(it.groups["arg"]?.value ?: ""))
                            PARG.command -> arguments.add(RequiredArgument(it.groups["arg"]?.value ?: ""))
                        }
                        counter += it.range.length + 1
                    }
                    else {
                        return@breaker
                    }
                }
            }

            // Special convention in LaTeX base
            if (arguments.isEmpty()) {
                run breaker@{
                    counter = counterInit
                    """\s*(?:\{\\meta\{(?<marg>.+?)}}|\[\\meta\{(?<oarg>.+?)}])""".toRegex().findAll(docs, counterInit).forEach { match ->
                        if (match.range.first == counter) {
                            match.groups["marg"]?.value?.let {
                                arguments.add(RequiredArgument(it))
                            }
                            match.groups["oarg"]?.value?.let {
                                arguments.add(OptionalArgument(it))
                            }
                            counter += match.range.length + 1
                        }
                        else {
                            return@breaker
                        }
                    }
                }
            }

            return arguments.toTypedArray()
        }

        /**
         * Given the [docs] of the [commandWithSlash], check if the parameters of the [commandWithSlash] are documented in [docs] and if so, return them.
         */
        fun extractArgumentsFromDocs(docs: String, commandWithSlash: String): Array<Argument> {
            // Maybe the arguments are given right at the beginning of the docs
            val argCommands = arrayOf(OARG, MARG, PARG).map { it.commandWithSlash }.toTypedArray()
            if (docs.startsWithAny(*argCommands)) {
                return getArgumentsFromStartOfString(docs, 0)
            }

            // Maybe the command appears somewhere in the docs with all the arguments after it
            // Check for each command in the docs,
            """\$commandWithSlash""".toRegex().findAll(docs).forEach { match ->
                // whether the arguments follow it.
                getArgumentsFromStartOfString(docs, match.range.last + 1).let { if (it.isNotEmpty()) return it }
            }

            return emptyArray()
        }

        /**
         * Looks up the given command within context.
         *
         * @param command The command PSI element to look up. Takes into account whether it is placed in math mode.
         * @return The found command, or `null` when the command does not exist.
         */
        fun lookup(command: LatexCommands): Set<LatexCommand>? {
            val cmdWithSlash = command.commandToken.text
            val cmdWithoutSlash = cmdWithSlash.substring(1)

            return if (command.inMathContext() && LatexMathCommand[cmdWithoutSlash] != null) {
                LatexMathCommand[cmdWithoutSlash]
            }
            else {
                // Attempt to avoid an error about slow operations on EDT
                runBlocking {
                    lookupInIndex(cmdWithoutSlash, command.project)
                }
            }
        }
    }

    /**
     * Uniquely identifies the command, when two commands are the same, but from different packages, the identifier
     * should be different.
     */
    val identifier: String
        get() = commandWithSlash

    override val description: String
        get() = identifier

    /**
     * Get the name of the command without the first backslash.
     */
    val command: String

    val commandWithSlash: String
        get() = "\\$command"

    /**
     * Get the display value of the command.
     */
    val display: String?

    /**
     * Get all the command arguments.
     */
    val arguments: Array<out Argument>

    /**
     * Whether this command must be used in math mode (`true`).
     */
    val isMathMode: Boolean

    /**
     * Concatenates all arguments to each other.
     *
     * @return e.g. `{ARG1}{ARG2}[ARG3]`
     */
    @Suppress("KDocUnresolvedReference")
    fun getArgumentsDisplay(): String {
        val sb = StringBuilder()
        for (arg in arguments) {
            sb.append(arg.toString())
        }

        return sb.toString()
    }

    /**
     * Checks whether `{}` must be automatically inserted in the auto complete.
     *
     * @return `true` to insert automatically, `false` not to insert.
     */
    fun autoInsertRequired() = arguments.filterIsInstance<RequiredArgument>().isNotEmpty()

    @Suppress("UNCHECKED_CAST")
    fun <T : Argument> getArgumentsOf(clazz: KClass<T>): List<T> {
        return arguments.asSequence()
            .filter { clazz.java.isAssignableFrom(it.javaClass) }
            .mapNotNull { it as? T }
            .toList()
    }

    fun <T : Argument> getArgumentsOf(clazz: Class<T>) = getArgumentsOf(clazz.kotlin)
}

internal fun String.asRequired(type: Argument.Type = Argument.Type.NORMAL) = RequiredArgument(this, type)

internal fun String.asOptional(type: Argument.Type = Argument.Type.NORMAL) = OptionalArgument(this, type)

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.STMARYRD

/**
 * @author Hannah Schellekens
 */
enum class LatexDelimiterCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = true,
    val collapse: Boolean = false
) : LatexCommand {

    LEFT_PARENTH("left(", display = "("),
    RIGHT_PARENTH("right)", display = ")"),
    LEFT_BRACKET("left[", display = "["),
    RIGHT_BRACKET("right]", display = "]"),
    LEFT_BRACE("left\\{", display = "{"),
    RIGHT_BRACE("right\\}", display = "}"),
    LEFT_ANGULAR("left<", display = "<"),
    RIGHT_ANGULAR("right>", display = ">"),
    LEFT_PIPE("left|", display = "|"),
    RIGHT_PIPE("right|", display = "|"),
    LEFT_DOUBLE_PIPE("left\\|", display = "||"),
    RIGHT_DOUBLE_PIPE("right\\|", display = "||"),
    LEFT_ANGLE_BRACKET("langle", display = "<"),
    RIGHT_ANGLE_BRACKET("rangle", display = ">"),
    LEFT_CEIL("lceil", display = "⌈"),
    RIGHT_CEIL("rceil", display = "⌉"),
    LEFT_FLOOR("lfloor", display = "⌊"),
    RIGHT_FLOOR("rfloor", display = "⌋"),
    LEFT_MOUSTACHE("lmoustache", display = "⎰"),
    RIGHT_MOUSTACHE("rmoustache", display = "⎱"),
    LEFT_GROUP("lgroup"),
    RIGHT_GROUP("rgroup"),
    LEFT_VERTICAL_LINE("lvert", dependency = AMSMATH),
    RIGHT_VERTICAL_LINE("rvert", dependency = AMSMATH),
    LEFT_DOUBLE_VERTICAL_LINES("lVert", dependency = AMSMATH),
    RIGHT_DOUBLE_VERTICAL_LINES("rVert", dependency = AMSMATH),
    LEFT_BAG("lbag", display = "⟅", dependency = STMARYRD),
    RIGHT_BAG("rbag", display = "⟆", dependency = STMARYRD),
    LEFT_BAG_BOLD("Lbag", display = "⟅", dependency = STMARYRD),
    RIGHT_BAG_BOLD("Rbag", display = "⟆", dependency = STMARYRD),
    LEFT_BRACKET_BOLD("llbracket", display = "⟦", dependency = STMARYRD),
    RIGHT_BRACKET_BOLD("rrbracket", display = "⟧", dependency = STMARYRD),
    LEFT_CEIL_BOLD("llceil", dependency = STMARYRD),
    RIGHT_CEIL_BOLD("rrceil", dependency = STMARYRD),
    LEFT_FLOOR_BOLD("llfloor", dependency = STMARYRD),
    RIGHT_FLOOR_BOLD("rrfloor", dependency = STMARYRD),
    LEFT_PARENTHESIS_BOLD("llparenthesis", display = "⦅", dependency = STMARYRD),
    RIGHT_PARENTHESIS_BOLD("rrparenthesis", display = "⦆", dependency = STMARYRD),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.EUROSYM

/**
 * @author Hannah Schellekens
 */
enum class LatexEuroCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    EURO_SYMBOL("euro", dependency = EUROSYM, display = "€"),
    EURO_AMOUNT("EUR", "amount".asRequired(), dependency = EUROSYM, display = "€"),
    EURO("geneuro", dependency = EUROSYM, display = "€"),
    EURO_NARROW("geneuronarrow", dependency = EUROSYM, display = "€"),
    EURO_WIDE("geneurowide", dependency = EUROSYM, display = "€"),
    OFFICIAL_EURO("officialeuro", dependency = EUROSYM, display = "€"),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSSYMB
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.WASYSYM

/**
 * @author Hannah Schellekens
 */
enum class LatexGenericMathCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = true,
    val collapse: Boolean = false
) : LatexCommand {

    ACUTE("acute", "a".asRequired()),
    ALEPH("aleph"),
    AMALGAMATION("amalg", display = "⨿", collapse = true),
    INVERSE_COSINE("arccos"),
    INVERSE_SINE("arcsin"),
    INVERSE_TANGENT("arctan"),
    ARGUMENT("arg"),
    ARROWVERT("arrowvert", display = "|", collapse = true),
    ASYMP("asymp", display = "≍", collapse = true),
    REVERSED_PRIME("backprime", dependency = AMSMATH, display = "‵", collapse = true),
    BACKSLASH("backslash", display = "\\", collapse = true),
    BAR("bar", "a".asRequired()),
    N_ARY_INTERSECTION("bigcap", display = "⋂", collapse = true),
    BIG_CIRCLE("bigcirc", display = "○", collapse = true),
    N_ARY_UNION("bigcup", display = "⋃", collapse = true),
    BIG_CIRCLED_DOT("bigodot", display = "⨀", collapse = true),
    BIG_CIRCLED_PLUS("bigoplus", display = "⨁", collapse = true),
    BIG_CIRCLED_TIMES("bigotimes", display = "⨂", collapse = true),
    N_ARY_SQUARE_UNION("bigsqcup", display = "⨆", collapse = true),
    BIGTRIANGLEDOWN("bigtriangledown", display = "▽", collapse = true),
    BIG_TRIANGLE_UP("bigtriangleup", display = "∆", collapse = true),
    BIG_TRIANGLE_DOWN("bigtrangledown", display = "▽", collapse = true),
    N_ARY_UNION_WITH_PLUS("biguplus", display = "⨄", collapse = true),
    BIG_VEE("bigvee", display = "⋁", collapse = true),
    BIG_WEDGE("bigwedge", display = "⋀", collapse = true),
    BIG_STAR("bigstar", dependency = AMSSYMB, display = "★", collapse = true),
    BINOM("binom", "total".asRequired(), "sample".asRequired(), dependency = AMSMATH),
    BLACKBOARD_BOLD_K("Bbbk", dependency = AMSSYMB, display = "\uD835\uDD5C", collapse = true),
    BLACK_SQUARE("blacksquare", dependency = AMSSYMB, display = "⬛", collapse = true),
    BLACK_LOZENGE("blacklozenge", dependency = AMSSYMB, display = "⧫", collapse = true),
    BLACK_TRIANGLE("blacktriangle", dependency = AMSSYMB, display = "▴", collapse = true),
    BLACK_TRIANGLE_DOWN("blacktriangledown", dependency = AMSSYMB, display = "▾", collapse = true),
    BM("bm", "text".asRequired(Argument.Type.TEXT), dependency = LatexPackage.BM),
    BMOD("bmod"),
    BOT("bot", display = "⟂", collapse = true),
    BRACEVERT("bracevert"),
    BREVE("breve", "a".asRequired()),
    CAPITAL_ARROWVERT("Arrowvert"),
    BOX("Box", dependency = AMSSYMB, display = "☐", collapse = true),
    AMSSYMB_DIAMOND("Diamond", dependency = AMSSYMB, display = "◇", collapse = true),
    MIRRORED_G("Game", dependency = AMSSYMB),
    PROBABILITY("Pr"),
    CDOT("cdot", display = "·", collapse = true),
    MIDDLE_DOT("cdotp", display = "·", collapse = true),
    CENTER_DOTS("cdots", display = "⋯", collapse = true),
    CHECK("check", "a".asRequired()),
    CIRCLED_R("circledR", dependency = AMSSYMB, display = "®", collapse = true),
    CIRCLED_S("circledS", dependency = AMSSYMB, display = "Ⓢ", collapse = true),
    CLOSING_BRACE("}", display = "}"),
    CLUB_SUIT("clubsuit", display = "♣", collapse = true),
    COLON("colon", display = ":", collapse = true),
    CONG("cong"),
    COSINE("cos"),
    HYPERBOLIC_COSINE("cosh"),
    COTANGENT("cot"),
    HYPERBOLIC_COTANGENT("coth"),
    COSECANT("csc"),
    DAGGER("dagger", display = "†", collapse = true),
    LEFT_TACK("dashv", display = "⊣", collapse = true),
    DOUBLE_DAGGER("ddagger", display = "‡", collapse = true),
    QUADRUPLE_DOTS("ddddot", "a".asRequired(), dependency = AMSMATH),
    TRIPLE_DOTS("dddot", "a".asRequired(), dependency = AMSMATH),
    DOUBLE_DOTS("ddot", "a".asRequired()),
    DOWN_RIGHT_DIAGONAL_DOTS("ddots", display = "⋱", collapse = true),
    DEGREES("deg"),
    DERMINANT("det"),
    DFRAC("dfrac", "num".asRequired(), "den".asRequired(), dependency = AMSMATH),
    DIAGONAL_DOWN("diagdown", dependency = AMSSYMB, display = "∖", collapse = true),
    DIAGONAL_UP("diagup", dependency = AMSSYMB, display = "∕", collapse = true),
    DIAMOND("diamond", display = "◇", collapse = true),
    DIAMOND_SUIT("diamondsuit", display = "♢", collapse = true),
    DIMENSION("dim"),
    DIV("div", display = "÷", collapse = true),
    DIVIDEONTIMES("divideontimes", dependency = AMSSYMB, display = "⋇", collapse = true),
    DOT("dot", "a".asRequired()),
    DOTEQ("doteq"),
    DOTS("dots", display = "⋯", collapse = true),
    DOUBLE_VERTICAL_ARROW("Arrowvert"),
    DOUBLE_VERTICAL_LINE("|", display = "‖", collapse = true),
    SCRIPT_SMALL_L("ell", display = "ℓ", collapse = true),
    ETH("eth", dependency = AMSSYMB, display = "ð", collapse = true),
    EXPONENTIAL("exp"),
    FLAT("flat", display = "♭", collapse = true),
    FRAC("frac", "num".asRequired(), "den".asRequired()),
    GREATEST_COMMON_DIVISOR("gcd"),
    GRAVE("grave", "a".asRequired()),
    HAT("hat", "a".asRequired()),
    H_BAR("hbar", display = "ℏ", collapse = true),
    HEART_SUIT("heartsuit", display = "♡", collapse = true),
    HOMOMORPHISM("hom"),
    H_SLASH("hslash", dependency = AMSSYMB, display = "ħ", collapse = true),
    IMAGINARY("Im", display = "ℑ", collapse = true),
    DOTLESS_I("imath", display = "ı", collapse = true),
    INFINUM("inf"),
    INTERTEXT("intertext", "text".asRequired(), dependency = AMSMATH),
    INVERSE_F("Finv", dependency = AMSSYMB, display = "Ⅎ", collapse = true),
    INVERSED_NOT_SIGN("invneg", dependency = WASYSYM),
    DOTLESS_J("jmath", display = "ȷ", collapse = true),
    KERNEL("ker"),
    LDOTP("ldotp"),
    LOWER_DOTS("ldots", display = "…", collapse = true),
    BASE_2_LOGARITHM("lg"),
    LIMIT("lim"),
    LIMIT_INFERIOR("liminf"),
    LIMIT_SUPERIOR("limsup"),
    NATURAL_LOGARITHM("ln"),
    LOGARITHM("log"),
    LOG_OF("logof", dependency = WASYSYM, display = "⍟", collapse = true),
    LOWER_LEFT_CORNER("llcorner", dependency = AMSSYMB, display = "⌞", collapse = true),
    LOWER_RIGHT_CORNER("lrcorner", dependency = AMSSYMB, display = "⌟", collapse = true),
    LOZENGE("lozenge", dependency = AMSSYMB, display = "⬨", collapse = true),
    MATHBB("mathbb", "text".asRequired(Argument.Type.TEXT), dependency = LatexPackage.AMSFONTS),
    MATHBF("mathbf", "text".asRequired()),
    MATHCAL("mathcal", "text".asRequired()),
    MATHDS("mathds", "mathds".asRequired()),
    MATHELLIPSIS("mathellipsis"),
    MATHFRAK("mathfrak", "text".asRequired(Argument.Type.TEXT), dependency = LatexPackage.AMSFONTS),
    MATHGROUP("mathgroup"),
    MATHIT("mathit", "text".asRequired()),
    MATHNORMAL("mathnormal", "text".asRequired()),
    MATHRING("mathring", "a".asRequired()),
    MATHRM("mathrm", "text".asRequired()),
    MATHSCR("mathscr"),
    MATHSF("mathsf", "text".asRequired()),
    MATHSTERLING("mathsterling"),
    MATHTT("mathtt", "text".asRequired()),
    MATHUNDERSCORE("mathunderscore"),
    MAXIMUM("max"),
    MINIMUM("min"),
    NATURAL("natural", display = "♮", collapse = true),
    NOT_SIGN("neg", display = "¬", collapse = true),
    O_CIRCLE("ocircle", dependency = WASYSYM, display = "◯", collapse = true),
    OPEN_BRACE("{", display = "{"),
    OVERBRACE("overbrace", "text".asRequired()),
    OVER_LEFT_ARROW("overleftarrow", "text".asRequired()),
    OVER_LEFT_RIGHT_ARROW("overleftrightarrow", "text".asRequired(), dependency = AMSMATH),
    OVERLINE("overline", "text".asRequired()),
    OVER_RIGHT_ARROW("overrightarrow", "text".asRequired()),
    PMOD("pmod"),
    PRIME("prime", display = "′", collapse = true),
    INVERSE_LIMIT("projlim", dependency = AMSMATH),
    REAL("Re", display = "ℜ", collapse = true),
    REVERSED_OHM("mho", dependency = AMSSYMB, display = "℧", collapse = true),
    SECANT("sec"),
    SHARP("sharp", display = "♯", collapse = true),
    SINE("sin"),
    HYPERBOLIC_SINE("sinh"),
    SPADE_SUIT("spadesuit", display = "♠", collapse = true),
    SQUARE_ROOT("sqrt", "root".asOptional(), "arg".asRequired()),
    SQUARE("square", dependency = AMSSYMB, display = "◻", collapse = true),
    STACKED_RELATION("stackrel", "top".asRequired(), "relation".asRequired()),
    SUPREMUM("sup"),
    SQUARE_ROOT_SURD("surd", display = "√", collapse = true),
    TANGENT("tan"),
    HBOLICTANGENT("tanh"),
    TEXT("text", "text".asRequired(), dependency = AMSMATH),
    TILDE("tilde", "a".asRequired()),
    TOP("top", display = "⊤", collapse = true),
    TRIANGLE_UP("triangle", display = "△", collapse = true),
    TRIANGLE_DOWN("triangledown", dependency = AMSSYMB, display = "▽", collapse = true),
    UNDERBRACE("underbrace", "text".asRequired()),
    UNDER_LEFT_ARROW("underleftarrow", "text".asRequired(), dependency = AMSMATH),
    UNDER_LEFT_RIGHT_ARROW("underleftrightarrow", "text".asRequired(), dependency = AMSMATH),
    UNDERLINE("underline", "text".asRequired()),
    UNDER_RIGHT_ARROW("underrightarrow", "text".asRequired(), dependency = AMSMATH),
    UNDERSCORE("_", display = "_"),
    UPPER_LEFT_CORNER("ulcorner", dependency = AMSSYMB, display = "⌜", collapse = true),
    UPPER_RIGHT_CORNER("urcorner", dependency = AMSSYMB, display = "⌝", collapse = true),
    DIRECT_LIMIT_VARIANT("varinjlim", dependency = AMSMATH),
    LIMIT_INFERIOR_VARIANT("varliminf", dependency = AMSMATH),
    LIMIT_SUPERIOR_VARIANT("varlimsup", dependency = AMSMATH),
    INVERSE_LIMIT_VARIANT("varprojlim", dependency = AMSMATH),
    TRIANGLE_VARIANT("vartriangle", dependency = AMSSYMB, display = "△", collapse = true),
    VERTICAL_DOTS("vdots", display = "⋮", collapse = true),
    VECTOR("vec", "a".asRequired()),
    VEE("vee", display = "⋁", collapse = true),
    VERTICAL_ARROW("arrowvert"),
    WASYSYM_PROPTO("wasypropto", dependency = WASYSYM, display = "∝", collapse = true),
    WEDGE("wedge", display = "⋀", collapse = true),
    WIDE_HAT("widehat", "text".asRequired()),
    WIDE_TILDE("widetilde", "text".asRequired()),
    SCRIPT_CAPITAL_P("wp", display = "℘", collapse = true),
    ABOVE_LEFT_ARROW("xleftarrow", "text".asRequired(), dependency = AMSMATH),
    ABOVE_RIGHT_ARROW("xrightarrow", "text".asRequired(), dependency = AMSMATH),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.BIBLATEX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.CLEVEREF
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.COLOR
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.CSQUOTES
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.FONTENC
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.GLOSSARIES
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.GRAPHICX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.MATHTOOLS
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.NTHEOREM
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.TEXTCOMP
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.ULEM
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.VARIOREF
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.XCOLOR

/**
 * @author Hannah Schellekens
 */
enum class LatexGenericRegularCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    ADDTOCOUNTER("addtocounter", "countername".asRequired(), "value".asRequired()),
    A_RING("aa", display = "å"),
    CAPITAL_A_RING("AA", display = "Å"),
    ADDBIBRESOURCE("addbibresource", RequiredFileArgument("bibliographyfile", true, false, "bib"), dependency = BIBLATEX),
    AE("ae", display = "æ"),
    CAPITAL_AE("AE", display = "Æ"),
    APPENDIX("appendix"),
    AUTHOR("author", "name".asRequired(Argument.Type.TEXT)),
    AUTOREF("autoref", "label".asRequired(Argument.Type.LABEL), dependency = LatexPackage.HYPERREF),
    AUTOREF_CAPITAL("Autoref", "label".asRequired(Argument.Type.LABEL), dependency = LatexPackage.HYPERREF),
    BEGIN("begin", "environment".asRequired()),
    END("end", "environment".asRequired()),
    ENSUREMATH("ensuremath", "text".asRequired()),
    BASELINESKIP("baselineskip"),
    BASELINESTRETCH("baselinestretch"),
    BF("bf"),
    BFSERIES("bfseries"),
    BIBITEM("bibitem", "label".asOptional(), "citekey".asRequired()),
    BIBLIOGRAPHYSTYLE("bibliographystyle", "style".asRequired(Argument.Type.BIBLIOGRAPHY_STYLE)),
    BIBLIOGRAPHY("bibliography", RequiredFileArgument("bibliographyfile", true, true, "bib")),
    BIGSKIP("bigskip"),
    BLENDCOLORS("blendcolors", "mix expr".asRequired(), dependency = XCOLOR),
    BLENDCOLORS_STAR("blendcolors*", "mix expr".asRequired(), dependency = XCOLOR),
    BOLDMATH("boldmath"),
    BOXFRAME("boxframe", "width".asRequired(), "height".asRequired(), "depth".asRequired(), dependency = XCOLOR),
    CAPTION("caption", "shorttext".asOptional(), "text".asRequired(Argument.Type.TEXT)),
    CAPTIONOF("captionof", "float type".asRequired(), "list entry".asOptional(), "heading".asRequired(Argument.Type.TEXT)),
    CHAPTER("chapter", "shorttitle".asOptional(), "title".asRequired(Argument.Type.TEXT)),
    CHAPTER_STAR("chapter*", "title".asRequired(Argument.Type.TEXT)),
    CITE("cite", "extratext".asOptional(), "keys".asRequired()),
    CLEARDOUBLEPAGE("cleardoublepage"),
    CLEARPAGE("clearpage"),
    COLOR_CMD("color", "color".asRequired(), dependency = COLOR),
    COLOR2("color", "model-list".asOptional(), "spec-list".asRequired(), dependency = XCOLOR),
    COLORBOX("colorbox", "color".asRequired(), "text".asRequired(), dependency = COLOR),
    COLORBOX2("colorbox", "model-list".asOptional(), "spec-list".asRequired(), "text".asRequired(), dependency = XCOLOR),
    COLORMASK("colormask", dependency = XCOLOR),
    COLORSERIESCYCLE("colorseriescycle", dependency = XCOLOR),
    COLUMNSEP("columnsep "),
    COLUMNWIDTH("columnwidth"),
    CONTENTSLINE("contentsline", "type".asRequired(), "text".asRequired(Argument.Type.TEXT), "page".asRequired()),
    CONTENTSNAME("contentsname", "name".asRequired()),
    CONVERTCOLORSPEC("convertcolorspec", "model".asRequired(), "spec".asRequired(), "target model".asRequired(), "cmd".asRequired(), dependency = XCOLOR),
    TEXT_DAGGER("dag", display = "†"),
    TEXT_DOUBLE_DAGGER("ddag", display = "‡"),
    DATE("date", "text".asRequired(Argument.Type.TEXT)),
    DECLARE_MATH_OPERATOR("DeclareMathOperator", "command".asRequired(), "operator".asRequired(Argument.Type.TEXT), dependency = AMSMATH),
    DEF("def"),
    DOCUMENTCLASS("documentclass", "options".asOptional(), RequiredFileArgument("class", true, false, "cls")),
    DOLLAR_SIGN("$", display = "$"),
    DOTFILL("dotfill"),
    EM("em"),
    EMPH("emph", "text".asRequired(Argument.Type.TEXT)),
    ENLARGETHISPAGE("enlargethispage", "size".asRequired()),
    ENLARGETHISPAGE_STAR("enlargethispage*", "size".asRequired()),
    ENQUOTE("enquote", dependency = CSQUOTES),
    ENQUOTE_STAR("enquote*", dependency = CSQUOTES),
    EQREF("eqref", "eqLabel".asRequired(Argument.Type.LABEL), dependency = AMSMATH),
    EVENSIDEMARGIN("evensidemargin"),
    EXTERNALDOCUMENT("externaldocument", "prefix".asOptional(), RequiredFileArgument("file", false, false, "tex")),
    EXTRACTCOLORSPEC("extractcolorspec", "color".asRequired(), "cmd".asRequired(), dependency = XCOLOR),
    EXTRACTCOLORSPECS("extractcolorspecs", "color".asRequired(), "model-cmd".asRequired(), "color-cmd".asRequired(), dependency = XCOLOR),
    FAMILY("family"),
    FBOX("fbox", "text".asRequired(Argument.Type.TEXT)),
    FCOLORBOX("fcolorbox", "frame color".asRequired(), "background color".asRequired(), "text".asRequired(), dependency = COLOR),
    FCOLORBOX2("fcolorbox", "model-list".asOptional(), "frame spec-list".asRequired(), "background spec-list".asRequired(), "text".asRequired(), dependency = XCOLOR),
    FCOLORBOX3("fcolorbox", "frame model-list".asOptional(), "frame spec-list".asRequired(), "background model-list".asOptional(), "background spec-list".asRequired(), "text".asRequired(), dependency = XCOLOR),
    FCOLORBOX4("fcolorbox", "frame color".asRequired(), "background model-list".asOptional(), "background spec-list".asRequired(), "text".asRequired(), dependency = XCOLOR),
    FIGURENAME("figurename", "name".asRequired(Argument.Type.TEXT)),
    FLQ("flq", display = "‹"),
    FLQQ("flqq", display = "«"),
    FLUSHBOTTOM("flushbottom"),
    FLUSHLEFT("flushleft"),
    FLUSHRIGHT("flushright"),
    FONTENCODING("fontencoding", "enc".asRequired()),
    FONTFAMILY("fontfamily", "family".asRequired()),
    FONTSERIES("fontseries", "series".asRequired()),
    FONTSHAPE("fontshape", "shape".asRequired()),
    FONTSIZE("fontsize", "size".asRequired(), "skip".asRequired()),
    FOOTNOTESIZE("footnotesize"),
    FOOTNOTETEXT("footnotetext", "number".asOptional(), "text".asRequired(Argument.Type.TEXT)),
    FOOTNOTE("footnote", "number".asOptional(), "text".asRequired(Argument.Type.TEXT)),
    FOOTNOTEMARK("footnotemark"),
    FRAMEBOX("framebox", "width".asOptional(), "pos".asOptional(), "text".asOptional(Argument.Type.TEXT)),
    FRAME("frame", "text".asRequired(Argument.Type.TEXT)),
    FRQ("frq", display = "›"),
    FRQQ("frqq", display = "»"),
    FULLREF("fullref", "label".asRequired(Argument.Type.LABEL), dependency = LatexPackage.HYPERREF),
    GLOSSARYENTRY("glossaryentry", "text".asRequired(Argument.Type.TEXT), "pagenum".asRequired()),
    GLOSSARY("glossary", "text".asRequired(Argument.Type.TEXT)),
    GLQ("glq", display = ","),
    GLQQ("glqq", display = "„"),
    GRAPHICSPATH("graphicspath", RequiredFolderArgument("foldername"), dependency = GRAPHICX),
    GRQ("grq", display = "‘"),
    GRQQ("grqq", display = "“"),
    GUILLEMOTLEFT("guillemotleft", dependency = FONTENC.with("T1"), display = "«"),
    GUILLEMOTRIGHT("guillemotright", dependency = FONTENC.with("T1"), display = "»"),
    HFILL("hfill"),
    HIDEROWCOLORS("hiderowcolors", dependency = XCOLOR),
    HREF("href", "url".asRequired(), "text".asRequired(), dependency = LatexPackage.HYPERREF),
    HRULE("hrule"),
    HRULEFILL("hrulefill"),
    HSPACE("hspace", "length".asRequired()),
    HSPACE_STAR("hspace*", "length".asRequired()),
    HSS("hss"),
    HUGE("huge"),
    CAPITAL_HUGE("Huge"),
    HYPERREF("hyperref", "options".asOptional(), "label".asRequired(Argument.Type.TEXT), dependency = LatexPackage.HYPERREF),
    HYPHENATION("hyphenation", "words".asRequired(Argument.Type.TEXT)),
    I("i", display = "i (dotless)"),
    IMPORT("import", RequiredFolderArgument("absolute path"), RequiredFileArgument("filename", false, false, "tex"), dependency = LatexPackage.IMPORT),
    INCLUDE("include", RequiredFileArgument("sourcefile", false, false, "tex")),
    INCLUDEFROM("includefrom", RequiredFolderArgument("absolute path"), RequiredFileArgument("filename", false, false, "tex"), dependency = LatexPackage.IMPORT),
    INPUT("input", RequiredFileArgument("sourcefile", true, false, "tex")),
    INPUTFROM("inputfrom", RequiredFolderArgument("absolute path"), RequiredFileArgument("filename", false, false, "tex"), dependency = LatexPackage.IMPORT),
    INCLUDEGRAPHICS("includegraphics", "key-val-list".asOptional(), RequiredPicturePathArgument("imagefile", true, false, "pdf", "png", "jpg", "eps", "tikz"), dependency = GRAPHICX),
    INCLUDEONLY("includeonly", RequiredFileArgument("sourcefile", false, true, "tex")),
    INDEXNAME("indexname", "name".asRequired()),
    INDEXSPACE("indexspace"),
    INDEX("intex", "entry".asRequired()),
    IT("it"),
    ITEM("item", "label".asOptional()),
    ITSHAPE("itshape"),
    LABEL("label", "key".asRequired()),
    LARGE("large"),
    CAPITAL_LARGE("Large"),
    SCREAMING_LARGE("LARGE"),
    LATEX("LaTeX", display = "LaTeX"),
    LATEXE("LaTeXe", display = "LaTeX2ε"),
    LBRACK("lbrack", display = "["),
    LPAREN("lparen", display = "(", dependency = MATHTOOLS),
    ELLIPSIS("ldots", display = "…"),
    LEFTEQN("lefteqn"),
    LET("let"),
    LFSERIES("lfseries"),
    LINEBREAK("linebreak", "number".asOptional()),
    LINETHICKNESS("linethickness", "dimension".asRequired()),
    LINEWIDTH("linewidth"),
    LISTFIGURENAME("listfigurename", "name".asRequired(Argument.Type.TEXT)),
    LISTFILES("listfiles"),
    LISTOFFIGURES("listoffigures"),
    LISTOFTABLES("listoftables"),
    LISTTABLENAME("listtablename", "name".asRequired(Argument.Type.TEXT)),
    LOADCLASS("LoadClass", "options".asOptional(), RequiredFileArgument("class", true, false, "cls")),
    LOADCLASSWITHOPTIONS("LoadClassWithOptions", RequiredFileArgument("class", true, false, "cls")),
    LOWERCASE("lowercase", "text".asRequired(Argument.Type.TEXT)),
    LQ("lq", display = "‘"),
    MARG("marg", "arg".asRequired()),
    MAKEGLOSSARY("makeglossary"),
    MAKEINDEX("makeindex"),
    MAKELABEL("makelabel"),
    MAKELABELS("makelabels", "number".asRequired()),
    MAKENOIDXGLOSSARIES("makenoidxglossaries", dependency = GLOSSARIES),
    MAKETITLE("maketitle"),
    MASKCOLORS("maskcolors", "num model".asOptional(), "color".asRequired(), dependency = XCOLOR),
    MBOX("mbox", "text".asRequired()),
    MDSERIES("mdseries"),
    MEDSKIP("medskip"),
    MULTICOLUMN("multicolumn", "cols".asRequired(), "pos".asRequired(), "text".asRequired(Argument.Type.TEXT)),
    NAMEREF("nameref", "label".asRequired(Argument.Type.LABEL), dependency = LatexPackage.HYPERREF),
    NEWLABEL("newlabel"),
    NEWLENGTH("newlength", "length".asRequired()),
    NEWLINE("newline"),
    NEWPAGE("newpage"),
    NEWTHEOREM("newtheorem", "envname".asRequired(), "numberedlike".asOptional(), "caption".asRequired(Argument.Type.TEXT), "within".asOptional()),
    NEWTHEOREM_STAR("newtheorem*", "envname".asRequired(), "caption".asRequired(Argument.Type.TEXT)),
    NOCITE("nocite", "keys".asRequired()),
    NOFILES("nofiles"),
    NOLINEBREAK("nolinebreak", "number".asOptional()),
    NONUMBER("nonumber"),
    NOPAGEBREAK("nopagebreak", "number".asOptional()),
    NOPAGECOLOR("nopagecolor", dependency = COLOR),
    NORMALCOLOR("normalcolor", dependency = COLOR),
    NORMALFONT("normalfont"),
    NORMALSIZE("normalsize"),
    OARG("oarg", "arg".asRequired()),
    OE("oe", display = "œ"),
    OLDSTYLEENUMS("oldstylenums", "number".asRequired()),
    ONLYIFSTANDALONE("onlyifstandalone", "code".asRequired()), // dependency = standalone, but either class or package
    CAPITAL_OE("OE", display = "Œ"),
    ODDSIDEMARGIN("oddsidemargin"),
    ONECOLUMN("onecolumn"),
    PAGEBREAK("pagebreak", "number".asOptional()),
    PAGECOLOR("pagecolor", "color".asRequired(), dependency = COLOR),
    PAGECOLOR2("pagecolor", "model-list".asOptional(), "spec-list".asRequired(), dependency = XCOLOR),
    PAGENAME("pagename"),
    PAGENUMBERING("pagenumbering", "numstyle".asRequired()),
    PAGEREF("pageref", "label".asRequired(Argument.Type.LABEL)),
    PAGESTYLE("pagestyle", "style".asRequired()),
    PAGETOTAL("pagetotal"),
    PAPERWIDTH("paperwidth"),
    PAPERHEIGHT("paperheight"),
    PARAGRAPH("paragraph", "shorttitle".asOptional(Argument.Type.TEXT), "title".asRequired(Argument.Type.TEXT)),
    PARAGRAPH_STAR("paragraph*", "title".asRequired(Argument.Type.TEXT)),
    PARAGRAPHMARK("paragraphmark"),
    PARBOX("parbox", "pos".asOptional(), "width".asRequired(), "text".asRequired(Argument.Type.TEXT)),
    PARINDENT("parindent"),
    PARG("parg", "arg".asRequired()),
    PARSKIP("parskip"),
    PART("part", "shorttitle".asOptional(Argument.Type.TEXT), "title".asRequired(Argument.Type.TEXT)),
    PART_STAR("part*", "title".asRequired(Argument.Type.TEXT)),
    PARTNAME("partname", "name".asRequired(Argument.Type.TEXT)),
    PDFINFO("pdfinfo", "info".asRequired(Argument.Type.TEXT)),
    POUNDS("pounds", display = "£"),
    PRINTBIBLIOGRAPHY("printbibliography", dependency = BIBLATEX),
    PRINTINDEX("printindex"),
    PRINTNOIDXGLOSSARIES("printnoidxglossaries", dependency = GLOSSARIES),
    PROVIDESCLASS("ProvidesClass"),
    PROVIDESPACKAGE("ProvidesPackage"),
    R("r", display = "˚ (accent)"),
    RBRACK("rbrack", display = "]"),
    RPAREN("rparen", display = ")", dependency = MATHTOOLS),
    REF("ref", "label".asRequired(Argument.Type.LABEL)),
    REFNAME("refname", "name".asRequired(Argument.Type.TEXT)),
    REQUIREPACKAGE("RequirePackage", "options".asOptional(), RequiredFileArgument("package", true, true, "sty")),
    RESETCOLORSERIES("resetcolorseries", "div".asOptional(), "name".asRequired(), dependency = XCOLOR),
    RIGHTHYPHENMIN("righthyphenmin"),
    RIGHTMARGIN("rightmargin"),
    RIGHTMARK("rightmark"),
    RM("rm"),
    RMFAMILY("rmfamily"),
    ROMAN("roman", "counter".asRequired()),
    ROTATEBOX("rotatebox", "key-val-list".asOptional(), "degrees".asRequired(), "text".asRequired(Argument.Type.TEXT), dependency = GRAPHICX),
    ROWCOLORS("rowcolors", "commands".asOptional(), "row".asRequired(), "odd-row color".asRequired(), "even-row color".asRequired(), dependency = XCOLOR),
    ROWCOLORS_STAR("rowcolors*", "commands".asOptional(), "row".asRequired(), "odd-row color".asRequired(), "even-row color".asRequired(), dependency = XCOLOR),
    ROWNUM("rownum", dependency = XCOLOR),
    CAPITAL_ROMAN("Roman", "counter".asRequired()),
    RULE("rule", "line".asOptional(), "width".asRequired(), "thickness".asRequired()),
    RQ("rq", display = "’"),
    SAMEPAGE("samepage"),
    SBOX("sbox", "cmd".asRequired(), "length".asRequired()),
    SC("sc"),
    SCRIPTSIZE("scriptsize"),
    SCSHAPE("scshape"),
    SECTION("section", "shorttitle".asOptional(Argument.Type.TEXT), "title".asRequired(Argument.Type.TEXT)),
    SECTION_STAR("section*", "title".asRequired(Argument.Type.TEXT)),
    SECTION_SIGN("S", display = "§"),
    SELECTFONT("selectfont"),
    SETCOUNTER("setcounter", "countername".asRequired(), "value".asRequired()),
    SETLENGTH("setlength", "cmd".asRequired(), "length".asRequired()),
    SFFAMILY("sffamily"),
    SHORTSTACK("shortstack", "pos".asOptional(), "text".asRequired(Argument.Type.TEXT)),
    SHOWROWCOLORS("showrowcolors", dependency = XCOLOR),
    SF("sf"),
    SL("sl"),
    SLSHAPE("slshape"),
    SMALL("small"),
    SMALLSKIP("smallskip"),
    SMASH("smash"),
    SOUT("sout", "strikethroughtext".asRequired(Argument.Type.TEXT), dependency = ULEM),
    SPACE("space"),
    STEPCOUNTER("stepcounter", "counter".asRequired()),
    STOP("stop"),
    STRETCH("stretch", "factor".asRequired()),
    SUBFILE("subfile", RequiredFileArgument("sourcefile", true, false, "tex"), dependency = LatexPackage.SUBFILES),
    SUBFILEINCLUDE("subfileinclude", RequiredFileArgument("sourcefile", true, false, "tex"), dependency = LatexPackage.SUBFILES),
    SUBIMPORT("subimport", RequiredFolderArgument("relative path"), RequiredFileArgument("filename", false, false, "tex"), dependency = LatexPackage.IMPORT),
    SUBINCLUDEFROM("subincludefrom", RequiredFolderArgument("relative path"), RequiredFileArgument("filename", false, false, "tex"), dependency = LatexPackage.IMPORT),
    SUBINPUTFROM("subinputfrom"),
    SUBITEM("subitem"),
    SUBPARAGRAPH("subparagraph", "shorttitle".asOptional(Argument.Type.TEXT), "title".asRequired(Argument.Type.TEXT)),
    SUBPARAGRAPH_STAR("subparagraph*", "title".asRequired(Argument.Type.TEXT)),
    SUBPARAGRAPHMARK("subparagraphmark", "code".asRequired()),
    SUBSECTION("subsection", "shorttitle".asOptional(Argument.Type.TEXT), "title".asRequired(Argument.Type.TEXT)),
    SUBSECTION_STAR("subsection*", "title".asRequired(Argument.Type.TEXT)),
    SUBSECTIONMARK("subsectionmark", "code".asRequired()),
    SUBSUBITEM("subsubitem"),
    SUBSUBSECTION("subsubsection", "shorttitle".asOptional(Argument.Type.TEXT), "title".asRequired(Argument.Type.TEXT)),
    SUBSUBSECTION_STAR("subsubsection*", "title".asRequired(Argument.Type.TEXT)),
    SUBSUBSECTIONMARK("subsubsectionmark", "code".asRequired()),
    SUPPRESSFLOATS("suppressfloats", "placement".asOptional()),
    SYMBOL("symbol", "n".asRequired()),
    TABCOLSEP("tabcolsep"),
    TABLENAME("tablename", "name".asRequired(Argument.Type.TEXT)),
    TABLEOFCONTENTS("tableofcontents"),
    TESTCOLOR("testcolor", "color".asRequired(), dependency = XCOLOR),
    TESTCOLOR2("testcolor", "model-list".asOptional(), "spec-list".asRequired(), dependency = XCOLOR),
    TEXT_ASCII_CIRCUMFLEX("textasciicircum", display = "^"),
    TEXT_ASCII_TILDE("textasciitilde", display = "~"),
    TEXT_CENTERED_ASTERISK("textasteriskcentered", display = "⁎"),
    TEXT_BACKSLASH("textbackslash", display = "\\"),
    TEXT_BAR("textbar", display = "|"),
    TEXTBF("textbf", "text".asRequired(Argument.Type.TEXT)),
    TEXTBRACELEFT("textbraceleft", display = "{"),
    TEXTBRACERIGHT("textbraceright", display = "}"),
    TEXT_BULLET("textbullet", display = "•"),
    CIRCLED_TEXT("textcircled", "a".asRequired()),
    TEXTCOLOR("textcolor", "color".asRequired(), "text".asRequired(), dependency = COLOR),
    TEXTCOLOR2("textcolor", "model-list".asOptional(), "spec-list".asRequired(), "text".asRequired(), dependency = XCOLOR),
    TEXT_COPYRIGHT("textcopyright", display = "©"),
    TEXTDAGGER("textdagger", display = "†"),
    TEXTDAGGERDBL("textdaggerdbl", display = "‡"),
    TEXT_DOLLAR("textdollar", display = "$"),
    TEXT_DOWN_ARROW("textdownarrow", dependency = TEXTCOMP, display = "↓"),
    TEXTELLIPSIS("textellipsis", display = "…"),
    TEXT_EM_DASH("textemdash", display = "—"),
    TEXT_EN_DASH("textendash", display = "–"),
    TEXT_UPSIDE_DOWN_EXCLAMATION("textexclamdown", display = "¡"),
    TEXT_GREATER_THAN("textgreater", display = ">"),
    TEXTHEIGHT("textheight"),
    TEXTIT("textit", "text".asRequired(Argument.Type.TEXT)),
    TEXT_LEFT_ARROW("textleftarrow", dependency = TEXTCOMP, display = "←"),
    TEXT_LESS_THAN("textless", display = "<"),
    TEXTLF("textlf", "text".asRequired(Argument.Type.TEXT)),
    TEXTMD("textmd", "text".asRequired(Argument.Type.TEXT)),
    TEXTNORMAL("textnormal"),
    TEXTPARAGRAPH("textparagraph"),
    TEXT_CENTERED_PERIOD("textperiodcentered", display = "·"),
    TEXT_UPSIDE_DOWN_QUESTION_MARK("textquestiondown", display = "¿"),
    TEXT_LEFT_DOUBLE_QUOTE("textquotedblleft", display = "“"),
    TEXT_RIGHT_DOUBLE_QUOTE("textquotedblright", display = "”"),
    TEXT_LEFT_QUOTE("textquoteleft", display = "‘"),
    TEXT_RIGHT_QUOTE("textquoteright", display = "’"),
    TEXT_REGISTERED("textregistered", display = "®"),
    TEXT_RIGHT_ARROW("textrightarrow", dependency = TEXTCOMP, display = "→"),
    TEXTRM("textrm", "text".asRequired(Argument.Type.TEXT)),
    TEXTSC("textsc", "textsc".asRequired(Argument.Type.TEXT)),
    TEXTSECTION("textsection", display = "§"),
    TEXTSF("textsf", "text".asRequired(Argument.Type.TEXT)),
    TEXTSL("textsl", "text".asRequired(Argument.Type.TEXT)),
    TEXTSTERLING("textsterling", display = "£"),
    TEXTSUBSCRIPT("textsubscript", "text".asRequired(Argument.Type.TEXT)),
    TEXTSUPERSCRIPT("textsuperscript", "text".asRequired(Argument.Type.TEXT)),
    TEXT_TRADEMARK("texttrademark", display = "™"),
    TEXTTT("texttt", "text".asRequired(Argument.Type.TEXT)),
    TEXTUNDERSCORE("textunderscore", display = "_"),
    TEXTUP("textup", "text".asRequired(Argument.Type.TEXT)),
    TEXT_UP_ARROW("textuparrow", dependency = TEXTCOMP, display = "↑"),
    TEXT_VISIBLE_SPACE("textvisiblespace", display = "␣"),
    TEXTWIDTH("textwidth"),
    THANKS("thanks", "to".asRequired(Argument.Type.TEXT)),
    THICKLINES("thicklines"),
    THINLINES("thinlines"),
    THISPAGESTYLE("thispagestyle", "style".asRequired()),
    THREF("thref", "label".asRequired(), dependency = NTHEOREM),
    TIME("time"),
    TINY("tiny"),
    TITLE("title", "text".asRequired(Argument.Type.TEXT)),
    TODAY("today"),
    TOPMARGIN("topmargin"),
    TT("tt"),
    TTFAMILY("ttfamily"),
    TWOCOLUMN("twocolumn", "text".asOptional(Argument.Type.TEXT)),
    UNBOLDMATH("unboldmath"),
    UNDERLINE("underline", "text".asRequired(Argument.Type.TEXT)),
    UNITLENGTH("unitlength"),
    UPPERCASE("uppercase", "text".asRequired(Argument.Type.TEXT)),
    UPSHAPE("upshape"),
    URL("url", "url".asRequired(), dependency = LatexPackage.HYPERREF),
    USEPACKAGE("usepackage", "options".asOptional(), RequiredFileArgument("package", true, true, "sty")),
    USEPGFPLOTSLIBRARY("usepgfplotslibrary", "libraries".asRequired()),
    USETIKZLIBRARY("usetikzlibrary", "libraries".asRequired()),
    VDOTS("vdots", display = "⋮"),
    VLINE("vline"),
    VREF("vref", "key".asRequired(Argument.Type.LABEL), dependency = VARIOREF),
    VREFRANGE("vrefrange", "start".asRequired(), "end".asRequired(), "text".asOptional(), dependency = VARIOREF),
    VSPACE("vspace", "length".asRequired()),
    VSPACE_STAR("vspace*", "length".asRequired()),
    WIDTH("width"),
    XGLOBAL("xglobal", dependency = XCOLOR),

    // Cleveref
    CREF("cref", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    CREF_CAPITAL("Cref", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    CREFRANGE("crefrange", "label1".asRequired(Argument.Type.LABEL), "label2".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    CPAGEREF("cpageref", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    CPAGEREF_CAPITAL("Cpageref", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    CPAGEREFRANGE("cpagerefrange", "label1".asRequired(Argument.Type.LABEL), "label2".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    CPAGEREFRANGE_CAPITAL("Cpagerefrange", "label1".asRequired(Argument.Type.LABEL), "label2".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    NAMECREF("namecref", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    NAMECREF_CAPITAL("nameCref", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    LCNAMECREF("lcnamecref", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    NAMECREFS("namecrefs", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    NAMECREFS_CAPITAL("nameCrefs", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    LCNAMECREFS("lcnamecrefs", "label".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    LABELCREF("labelcref", "key".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    LABELCPAGEREF("labelcpageref", "key".asRequired(Argument.Type.LABEL), dependency = CLEVEREF),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.requiredParameters

enum class LatexGlossariesCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.GLOSSARIES,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    LONGNEWGLOSSARYENTRY(
        "longnewglossaryentry",
        "name".asRequired(),
        "options".asRequired(),
        "description".asRequired(),
        dependency = LatexPackage.GLOSSARIES
    ),
    NEWGLOSSARYENTRY(
        "newglossaryentry",
        "name".asRequired(),
        "options".asRequired(),
        dependency = LatexPackage.GLOSSARIES
    ),
    NEWACRONYM(
        "newacronym",
        "options".asOptional(),
        "name".asRequired(),
        "short".asRequired(),
        "long".asRequired(),
        dependency = LatexPackage.GLOSSARIES
    ),
    NEWABBREVIATION(
        "newabbreviation",
        "options".asOptional(),
        "name".asRequired(),
        "short".asRequired(),
        "long".asRequired(),
        dependency = LatexPackage.GLOSSARIES
    ),
    GLS("gls", "label".asRequired(), dependency = LatexPackage.GLOSSARIES),
    GLSUPPER("Gls", "label".asRequired(), dependency = LatexPackage.GLOSSARIES),
    GLSPLURAL("glspl", "label".asRequired(), dependency = LatexPackage.GLOSSARIES),
    GLSPLURALUPPER("Glspl", "label".asRequired(), dependency = LatexPackage.GLOSSARIES),

    LOADGLSENTRIES(
        "loadglsentries",
        RequiredFileArgument(
            "glossariesfile",
            isAbsolutePathSupported = true,
            commaSeparatesArguments = false
        ),
        dependency = LatexPackage.GLOSSARIES
    );

    companion object {

        /**
         * Extract the label text from a glossary entry command
         */
        fun extractGlossaryLabel(command: LatexCommands): String? {
            if (!CommandMagic.glossaryEntry.contains(command.name) &&
                !CommandMagic.glossaryReference.contains(command.name)
            ) return null
            return command.getRequiredParameters()[0]
        }

        /**
         * Extract the label element from a glossary entry command
         */
        fun extractGlossaryLabelElement(command: LatexCommands): PsiElement? {
            if (!CommandMagic.glossaryEntry.contains(command.name)) return null
            return command.requiredParameters()[0].firstChildOfType(LatexParameterText::class)
        }
    }
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSMATH

/**
 * @author Hannah Schellekens
 */
enum class LatexGreekCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = true,
    val collapse: Boolean = false
) : LatexCommand {

    ALPHA("alpha", display = "α", collapse = true),
    BETA("beta", display = "β", collapse = true),
    GAMMA("gamma", display = "γ", collapse = true),
    CAPITAL_GAMMA("Gamma", display = "Γ", collapse = true),
    CAPITAL_GAMMA_VARIANT("varGamma", dependency = AMSMATH, collapse = true),
    DELTA("delta", display = "δ", collapse = true),
    CAPITAL_DELTA("Delta", display = "Δ", collapse = true),
    CAPITAL_DELTA_VARIANT("varDelta", dependency = AMSMATH, collapse = true),
    EPSILON("epsilon", display = "ϵ", collapse = true),
    BETTER_LOOKING_EPSILON("varepsilon", display = "ε", collapse = true),
    ZETA("zeta", display = "ζ", collapse = true),
    ETA("eta", display = "η", collapse = true),
    THETA("theta", display = "θ", collapse = true),
    THETA_VARIANT("vartheta", display = "ϑ", collapse = true),
    CAPITAL_THETA("Theta", display = "Θ", collapse = true),
    CAPITAL_THETA_VARIANT("varTheta", dependency = AMSMATH, collapse = true),
    IOTA("iota", display = "ι", collapse = true),
    KAPPA("kappa", display = "κ", collapse = true),
    LAMBDA("lambda", display = "λ", collapse = true),
    CAPITAL_LAMBDA("Lambda", display = "Λ", collapse = true),
    CAPITAL_LAMBDA_VARIANT("varLambda", dependency = AMSMATH, collapse = true),
    MU("mu", display = "μ", collapse = true),
    NU("nu", display = "ν", collapse = true),
    XI("xi", display = "ξ", collapse = true),
    CAPITAL_XI("Xi", display = "Ξ", collapse = true),
    CAPITAL_XI_VARIANT("varXi", dependency = AMSMATH, collapse = true),
    PI("pi", display = "π", collapse = true),
    PI_VARIANT("varpi", display = "ϖ", collapse = true),
    CAPITAL_PI("Pi", display = "Π", collapse = true),
    CAPITAL_PI_VARIANT("varPi", dependency = AMSMATH, collapse = true),
    RHO("rho", display = "ρ", collapse = true),
    RHO_VARIANT("varrho", display = "ϱ", collapse = true),
    SIGMA("sigma", display = "σ", collapse = true),
    FINAL_SIGMA("varsigma", display = "ς", collapse = true),
    CAPITAL_SIGMA("Sigma", display = "Σ", collapse = true),
    CAPITAL_SIGMA_VARIANT("varSigma", dependency = AMSMATH, collapse = true),
    TAU("tau", display = "τ", collapse = true),
    UPSILON("upsilon", display = "υ", collapse = true),
    CAPITAL_UPSILON("Upsilon", display = "ϒ", collapse = true),
    CAPITAL_UPSILON_VARIANT("Upsilon", dependency = AMSMATH, collapse = true),
    PHI_STRAIGHT("phi", display = "φ", collapse = true),
    PHI_CURLY("varphi", display = "ϕ", collapse = true),
    CAPITAL_PHI("Phi", display = "Φ", collapse = true),
    CAPITAL_PHI_VARIANT("varPhi", dependency = AMSMATH, collapse = true),
    CHI("chi", display = "χ", collapse = true),
    PSI("psi", display = "ψ", collapse = true),
    CAPITAL_PSI("Psi", display = "Ψ", collapse = true),
    CAPITAL_PSI_VARIANT("varPsi", dependency = AMSMATH, collapse = true),
    OMEGA("omega", display = "ω", collapse = true),
    CAPITAL_OMEGA("Omega", display = "Ω", collapse = true),
    CAPITAL_OMEGA_VARIANT("varOmega", dependency = AMSMATH, collapse = true),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage

/**
 * [Source](http://mirrors.ctan.org/info/texbytopic/TeXbyTopic.pdf) chapter Conditionals
 *
 * @author Hannah Schellekens
 */
enum class LatexIfCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    IF("if"),
    IFCAT("ifcat"),
    IFX("ifx"),
    IFCASE("ifcase"),
    IFNUM("ifnum"),
    IFODD("ifodd"),
    IFHMODE("ifhmode"),
    IFVMODE("ifvmode"),
    IFMMODE("ifmmode"),
    IFINNER("ifinner"),
    IFDIM("ifdim"),
    IFVOID("ifvoid"),
    IFHBOX("ifhbox"),
    IFVBOX("ifvbox"),
    IFEOF("ifeof"),
    IFTRUE("iftrue"),
    IFFALSE("iffalse"),
    FI("fi"),
    ELSE("else"),
    OR("or"),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.LISTINGS
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.LUACODE
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.PYTHONTEX

/**
 * @author Hannah Schellekens
 */
enum class LatexListingCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    LSTINPUTLISTING("lstinputlisting", "options".asOptional(), RequiredFileArgument("filename", false, commaSeparatesArguments = false), dependency = LISTINGS),
    DIRECTLUA("directlua", "lua code".asRequired(), dependency = LUACODE),
    LUAEXEC("luaexec", "lua code".asRequired(), dependency = LUACODE),
    PY("py", dependency = PYTHONTEX),
    PYB("pyb", dependency = PYTHONTEX),
    PYC("pyc", dependency = PYTHONTEX),
    PYS("pys", dependency = PYTHONTEX),
    PYV("pyv", dependency = PYTHONTEX),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.BLINDTEXT
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.DEFAULT

/**
 * @author Hannah Schellekens
 */
enum class LatexLoremIpsumCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    BLIND_DOCUMENT("blinddocument", dependency = BLINDTEXT),
    LONG_BLIND_DOCUMENT("Blinddocument", dependency = BLINDTEXT),
    BLIND_TEXT("blindtext", "repetitions".asOptional(), dependency = BLINDTEXT),
    LONG_BLIND_TEXT("Blindtext", "paragraphs".asOptional(), "repetitions".asOptional(), dependency = BLINDTEXT),
    BLIND_LIST("blindlist", "list".asRequired(Argument.Type.LIST_ENVIRONMENT), dependency = BLINDTEXT),
    BLIND_LIST_OPTIONAL("blindlistoptional", "list".asRequired(Argument.Type.LIST_ENVIRONMENT), dependency = BLINDTEXT),
    BLIND_LIST_LIST("blindlistlist", "level".asOptional(), "list".asRequired(Argument.Type.LIST_ENVIRONMENT), dependency = BLINDTEXT),
    LONG_BLIND_LIST("Blindlist", "list".asRequired(Argument.Type.LIST_ENVIRONMENT), dependency = BLINDTEXT),
    LONG_BLIND_LIST_OPTIONAL("Blindlistoptional", "list".asRequired(Argument.Type.LIST_ENVIRONMENT), dependency = BLINDTEXT),
    BLIND_ITEMIZE("blinditemize", dependency = BLINDTEXT),
    BLIND_ENUMERATE("blindenumerate", dependency = BLINDTEXT),
    BLIND_DESCRIPTION("blinddescription", dependency = BLINDTEXT),
    LIPSUM("lipsum", "paragraph range".asOptional(), "sentence range".asOptional(), dependency = LatexPackage.LIPSUM),
    LIPSUM_AS_SINGLE_PARAGRAPH("lipsum*", "paragraph range".asOptional(), "sentence range".asOptional(), dependency = LatexPackage.LIPSUM),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import arrow.core.NonEmptySet
import arrow.core.getOrNone
import arrow.core.nonEmptySetOf

/**
 * @author Hannah Schellekens, Sten Wessel
 */
object LatexMathCommand {

    private val GREEK_ALPHABET: Set<LatexCommand> = LatexGreekCommand.values().toSet()
    private val OPERATORS: Set<LatexCommand> = LatexOperatorCommand.values().toSet()
    private val MATHTOOLS_COLONEQ: Set<LatexCommand> = LatexColoneqCommand.values().toSet()
    private val DELIMITERS: Set<LatexCommand> = LatexDelimiterCommand.values().toSet()
    private val ARROWS: Set<LatexCommand> = LatexArrowCommand.values().toSet()
    private val GENERIC_COMMANDS: Set<LatexCommand> = LatexGenericMathCommand.values().toSet()
    private val UNCATEGORIZED_STMARYRD_SYMBOLS: Set<LatexCommand> = LatexUncategorizedStmaryrdSymbols.values().toSet()

    private val ALL: Set<LatexCommand> = GREEK_ALPHABET + OPERATORS + MATHTOOLS_COLONEQ + DELIMITERS + ARROWS +
        GENERIC_COMMANDS + UNCATEGORIZED_STMARYRD_SYMBOLS

    private val lookup = HashMap<String, NonEmptySet<LatexCommand>>()
    private val lookupDisplay = HashMap<String, NonEmptySet<LatexCommand>>()

    init {
        ALL.forEach { cmd ->
            lookup[cmd.command] = lookup.getOrNone(cmd.command).fold({ nonEmptySetOf(cmd) }, { it + nonEmptySetOf(cmd) })
            if (cmd.display != null) {
                lookupDisplay[cmd.display!!] = lookupDisplay.getOrNone(cmd.display!!).fold({ nonEmptySetOf(cmd) }, { it + nonEmptySetOf(cmd) })
            }
        }
    }

    @JvmStatic
    fun values() = ALL

    @JvmStatic
    operator fun get(command: String) = lookup[command]

    @JvmStatic
    fun findByDisplay(display: String) = lookupDisplay[display]
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.MATHTOOLS

/**
 * @author Hannah Schellekens
 */
enum class LatexMathtoolsRegularCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    DECLARE_PAIRED_DELIMITER("DeclarePairedDelimiter", "cmd".asRequired(), "left delimiter".asRequired(), "right delimiter".asRequired(), dependency = MATHTOOLS),
    DECLARE_PAIRED_DELIMITER_X("DeclarePairedDelimiterX", "cmd".asRequired(), "num args".asOptional(), "left delimiter".asRequired(), "right delimiter".asRequired(), "body".asRequired(Argument.Type.TEXT), dependency = MATHTOOLS),
    DECLARE_PAIRED_DELIMITER_XPP("DeclarePairedDelimiterXPP", "cmd".asRequired(), "num args".asOptional(), "pre code".asRequired(), "left delimiter".asRequired(), "right delimiter".asRequired(), "post code".asRequired(), "body".asRequired(Argument.Type.TEXT), dependency = MATHTOOLS),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.NATBIB

/**
 * @author Hannah Schellekens
 */
enum class LatexNatbibCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    CITEP("citep", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEP_STAR("citep*", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITET("citet", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITET_STAR("citet*", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEP_CAPITALIZED("Citep", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEP_STAR_CAPITALIZED("Citep*", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITET_CAPITALIZED("Citet", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITET_STAR_CAPITALIZED("Citet*", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEALP("citealp", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEALP_STAR("citealp*", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEALT("citealt", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEALT_STAR("citealt*", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEALP_CAPITALIZED("Citealp", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEALP_STAR_CAPITALIZED("Citealp*", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEALT_CAPITALIZED("Citealt", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEALT_STAR_CAPITALIZED("Citealt*", "before".asOptional(), "after".asOptional(), "keys".asRequired(), dependency = NATBIB),
    CITEAUTHOR("citeauthor", "keys".asRequired(), dependency = NATBIB),
    CITEAUTHOR_STAR("citeauthor*", "keys".asRequired(), dependency = NATBIB),
    CITEAUTHOR_CAPITALIZED("Citeauthor", "keys".asRequired(), dependency = NATBIB),
    CITEAUTHOR_STAR_CAPITALIZED("Citeauthor*", "keys".asRequired(), dependency = NATBIB),
    CITEYEAR("citeyear", "keys".asRequired(), dependency = NATBIB),
    CITEYEAR_STAR("citeyear*", "keys".asRequired(), dependency = NATBIB),
    CITEYEARPAR("citeyearpar", "keys".asRequired(), dependency = NATBIB),
    CITETITLE("citetitle", "keys".asRequired(), dependency = NATBIB),
    CITETITLE_STAR("citetitle*", "keys".asRequired(), dependency = NATBIB),
    CITENUM("citenum", "key".asRequired(), dependency = NATBIB),
    CITETEXT("citetext", "text".asRequired(), dependency = NATBIB),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.TCOLORBOX

/**
 * @author Hannah Schellekens
 */
enum class LatexNewDefinitionCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    CATCODE("catcode"),
    NEWCOMMAND("newcommand", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Argument.Type.TEXT)),
    NEWCOMMAND_STAR("newcommand*", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Argument.Type.TEXT)),
    NEWIF("newif", "cmd".asRequired()),
    PROVIDECOMMAND("providecommand", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Argument.Type.TEXT)),
    PROVIDECOMMAND_STAR("providecommand*", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Argument.Type.TEXT)),
    RENEWCOMMAND("renewcommand", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Argument.Type.TEXT)),
    RENEWCOMMAND_STAR("renewcommand*", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Argument.Type.TEXT)),
    NEWENVIRONMENT("newenvironment", "name".asRequired(), "args".asOptional(), "default".asOptional(), "begdef".asRequired(Argument.Type.TEXT), "enddef".asRequired(Argument.Type.TEXT)),
    RENEWENVIRONMENT("renewenvironment", "name".asRequired(), "args".asOptional(), "default".asOptional(), "begdef".asRequired(Argument.Type.TEXT), "enddef".asRequired(Argument.Type.TEXT)),
    NEWTCOLORBOX("newtcolorbox", "init options".asOptional(), "name".asRequired(), "number".asOptional(), "default".asOptional(), "options".asRequired(), dependency = TCOLORBOX),
    RENEWTCOLORBOX("newtcolorbox", "init options".asOptional(), "name".asRequired(), "number".asOptional(), "default".asOptional(), "options".asRequired(), dependency = TCOLORBOX),
    DECLARETCOLORBOX("DeclareTColorBox", "init options".asOptional(), "name".asRequired(), "specification".asRequired(), "options".asRequired(), dependency = TCOLORBOX),
    NEWTCOLORBOX_("NewTColorBox", "init options".asOptional(), "name".asRequired(), "specification".asRequired(), "options".asRequired(), dependency = TCOLORBOX),
    RENEWTCOLORBOX_("ReNewTColorBox", "init options".asOptional(), "name".asRequired(), "specification".asRequired(), "options".asRequired(), dependency = TCOLORBOX),
    PROVIDETCOLORBOX("ProvideTColorBox", "init options".asOptional(), "name".asRequired(), "specification".asRequired(), "options".asRequired(), dependency = TCOLORBOX),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSSYMB
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.STMARYRD

/**
 * @author Hannah Schellekens
 */
enum class LatexOperatorCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = true,
    val collapse: Boolean = false
) : LatexCommand {

    FOR_ALL("forall", display = "∀", collapse = true),
    PARTIAL("partial", display = "∂", collapse = true),
    EXISTS("exists", display = "∃", collapse = true),
    NOT_EXISTS("nexists", dependency = AMSSYMB, display = "∄", collapse = true),
    EMPTY_SET("emptyset", display = "∅", collapse = true),
    NOTHING("varnothing", dependency = AMSSYMB, display = "∅", collapse = true),
    NABLA("nabla", display = "∇", collapse = true),
    ELEMENT_OF("in", display = "∈", collapse = true),
    NOT_ELEMENT_OF("notin", display = "∉", collapse = true),
    CONTAIN_AS_MEMBER("ni", display = "∋", collapse = true),
    COMPLEMENT("complement", dependency = AMSSYMB, display = "∁"),
    N_ARY_PRODUCT("prod", display = "∏", collapse = true),
    N_ARY_COPRODUCT("coprod", display = "∐", collapse = true),
    SUM("sum", display = "∑", collapse = true),
    MINUS_PLUS("mp", display = "∓", collapse = true),
    PLUS_MINUS("pm", display = "±", collapse = true),
    SET_MINUS("setminus", display = "∖", collapse = true),
    SMALL_SET_MINUS("smallsetminus", dependency = AMSSYMB, display = "∖", collapse = true),
    ASTERISK("ast", display = "∗"),
    STAR("star", display = "⋆", collapse = true),
    DOT_PLUS("dotplus", dependency = AMSSYMB, display = "∔"),
    CIRCLE("circ", display = "∘"),
    BULLET("bullet", display = "∙"),
    PROPORTIONAL_TO("propto", display = "∝", collapse = true),
    PROPORTIONAL_TO_SYMBOL("varpropto", dependency = AMSSYMB, display = "∝", collapse = true),
    INFINITY("infty", display = "∞", collapse = true),
    ANGLE("angle", display = "∠", collapse = true),
    MEASURED_ANGLE("measuredangle", dependency = AMSSYMB, display = "∡", collapse = true),
    SPHERICAL_ANGLE("sphericalangle", dependency = AMSSYMB, display = "∢", collapse = true),
    MID("mid", display = "∣", collapse = true),
    MID_SHORT("shortmid", dependency = AMSSYMB, display = "∣", collapse = true),
    NOT_MID_SHORT("nshortmid", dependency = AMSSYMB),
    PARALLEL("parallel", display = "∥", collapse = true),
    NOT_PARALLEL("nparallel", dependency = AMSSYMB, display = "∦", collapse = true),
    PARALLEL_SHORT("shortparallel", display = "∥", collapse = true),
    NOT_PARALLEL_SHORT("nshortparallel", dependency = AMSSYMB, display = "∦", collapse = true),
    LOGICAL_AND("land", display = "∧", collapse = true),
    LOGICAL_OR("lor", display = "∨", collapse = true),
    INTERSECTION("cap", display = "∩", collapse = true),
    UNION("cup", display = "∪", collapse = true),
    DOUBLE_UNION("Cup", dependency = AMSSYMB, display = "⋓", collapse = true),
    DOUBLE_INTERSECTION("Cap", dependency = AMSSYMB, display = "⋒", collapse = true),
    INTEGRAL("int", display = "∫", collapse = true),
    DOUBLE_INTEGRAL("iint", dependency = LatexPackage.AMSMATH, display = "∬", collapse = true),
    TRIPLE_INTEGRAL("iiint", dependency = LatexPackage.AMSMATH, display = "∭", collapse = true),
    QUADRUPLE_INTEGRAL("iiiint", dependency = LatexPackage.AMSMATH, display = "⨌", collapse = true),
    DOTS_INTEGRAL("idotsint", display = "∫⋯∫", collapse = true),
    CONTOUR_INTEGRAL("oint", display = "∮", collapse = true),
    THEREFORE("therefore", dependency = AMSSYMB, display = "∴", collapse = true),
    BECAUSE("because", dependency = AMSSYMB, display = "∵", collapse = true),
    TILDE_OPERATOR("sim", display = "∼", collapse = true),
    WREATH_PRODUCT("wr", display = "≀", collapse = true),
    APPROX("approx", display = "≈", collapse = true),
    NOT_EQUAL("neq", display = "≠", collapse = true),
    EQUIVALENT("equiv", display = "≡", collapse = true),
    LESS_THAN_EQUAL("leq", display = "≤", collapse = true),
    LESS_THAN_NOT_EQUAL("lneq", dependency = AMSSYMB, display = "⪇", collapse = true),
    LESS_THAN_EQUALL("leqq", dependency = AMSSYMB, display = "≦", collapse = true),
    GREATER_THAN_EQUAL("geq", display = "≥", collapse = true),
    GREATER_THAN_NOT_EQUAL("gneq", dependency = AMSSYMB, display = "⪈", collapse = true),
    GREATER_THAN_EQUALL("geqq", dependency = AMSSYMB, display = "≧", collapse = true),
    NOT_LESS_THAN("nless", dependency = AMSSYMB, display = "≮", collapse = true),
    NOT_GREATER_THAN("ngtr", dependency = AMSSYMB, display = "≯", collapse = true),
    NOT_LESS_THAN_EQUAL("nleq", dependency = AMSSYMB, display = "≰", collapse = true),
    NOT_LESS_THAN_EQUALL("nleqq", dependency = AMSSYMB, display = "≦\u200D\u0338"),
    NOT_GREATER_THAN_EQUAL("ngeq", dependency = AMSSYMB, display = "≱", collapse = true),
    NOT_GREATER_THAN_EQUALL("ngeqq", dependency = AMSSYMB, display = "≧\u200D\u0338"),
    DOUBLE_LESS_THAN("ll", dependency = AMSSYMB, display = "≪", collapse = true),
    LESS_LESS_LESS("lll", dependency = AMSSYMB, display = "⋘", collapse = true),
    LESS_NOT_EQUAL("lneqq", dependency = AMSSYMB, display = "≨", collapse = true),
    GREATER_NOT_EQUAL("gneqq", dependency = AMSSYMB, display = "≩", collapse = true),
    DOUBLE_GREATER_THAN("gg", dependency = AMSSYMB, display = "≫", collapse = true),
    GREATER_GREATER_GREATER("ggg", dependency = AMSSYMB, display = "⋙", collapse = true),
    SUBSET("subset", display = "⊂", collapse = true),
    SUPERSET("supset", display = "⊃", collapse = true),
    SUBSET_EQUALS("subseteq", display = "⊆", collapse = true),
    SUBSET_EQUALSS("subseteqq", dependency = AMSSYMB, display = "⊆", collapse = true),
    SUPERSET_EQUALS("supseteq", display = "⊇", collapse = true),
    SUPERSET_EQUALSS("supseteqq", dependency = AMSSYMB, display = "⊇", collapse = true),
    NOT_SUBSET_EQUALS("nsubseteq", dependency = AMSSYMB, display = "⊈", collapse = true),
    NOT_SUBSET_EQUALSS("nsubseteqq", dependency = AMSSYMB, display = "⊈", collapse = true),
    NOT_SUPERSET_EQUALS("nsupseteq", dependency = AMSSYMB, display = "⊉", collapse = true),
    NOT_SUPERSET_EQUALSS("nsupseteqq", dependency = AMSSYMB, display = "⊉", collapse = true),
    SQUARE_SUBSET("sqsubset", dependency = AMSSYMB, display = "⊏", collapse = true),
    SQUARE_SUPERSET("sqsupset", dependency = AMSSYMB, display = "⊐", collapse = true),
    SQUARE_SUBSET_EQUALS("sqsubseteq", dependency = AMSSYMB, display = "⊑", collapse = true),
    SQUARE_SUPERSET_EQUALS("sqsupseteq", dependency = AMSSYMB, display = "⊒", collapse = true),
    SQUARE_CAP("sqcap", display = "⊓", collapse = true),
    SQUARE_CUP("sqcup", display = "⊔", collapse = true),
    CIRCLED_PLUS("oplus", display = "⊕", collapse = true),
    CIRCLED_MINUS("ominus", display = "⊖", collapse = true),
    CIRCLED_TIMES("otimes", display = "⊗", collapse = true),
    CIRCLED_SLASH("oslash", display = "⊘", collapse = true),
    CIRCLED_DOT("odot", display = "⊙", collapse = true),
    BOXED_PLUS("boxplus", dependency = AMSSYMB, display = "⊞", collapse = true),
    BOXED_MINUS("boxminus", dependency = AMSSYMB, display = "⊟", collapse = true),
    BOXED_TIMES("boxtimes", dependency = AMSSYMB, display = "⊠", collapse = true),
    BOXED_DOT("boxdot", dependency = AMSSYMB, display = "⊡", collapse = true),
    BOWTIE("bowtie", display = "⋈", collapse = true),
    JOIN("Join", dependency = AMSSYMB, display = "⨝", collapse = true),
    TRIANGLE_RIGHT("triangleright", dependency = AMSSYMB, display = "▷", collapse = true),
    TRIANGLE_LEFT("triangleleft", dependency = AMSSYMB, display = "◁", collapse = true),
    LHD("lhd", dependency = AMSSYMB, display = "◁", collapse = true),
    RHD("rhd", dependency = AMSSYMB, display = "▷", collapse = true),
    UN_LHD("unlhd", dependency = AMSSYMB, display = "⊴", collapse = true),
    UN_RHD("unrhd", dependency = AMSSYMB, display = "⊵", collapse = true),
    TRIANGLELEFTEQ("tranglelefteq", dependency = AMSSYMB, display = "⊴", collapse = true),
    TRIANGLERIGHTEQ("trianglerighteq", dependency = AMSSYMB, display = "⊵", collapse = true),
    LTIMES("ltimes", dependency = AMSSYMB, display = "⋉", collapse = true),
    RTIMES("rtimes", dependency = AMSSYMB, display = "⋊", collapse = true),
    TIMES("times", display = "×", collapse = true),
    LEFT_THREE_TIMES("leftthreetimes", dependency = AMSSYMB, display = "⋋", collapse = true),
    RIGHT_THREE_TIMES("rightthreetimes", dependency = AMSSYMB, display = "⋌", collapse = true),
    CIRCLED_CIRCLE("circledcirc", dependency = AMSSYMB, display = "⊚", collapse = true),
    CIRCLED_DASH("circleddash", dependency = AMSSYMB, display = "⊝", collapse = true),
    CIRCLED_ASTERISK("circledast", dependency = AMSSYMB, display = "⊛", collapse = true),
    MULTISET_UNION("uplus", display = "⊎", collapse = true),
    WEDGE_BAR("barwedge", dependency = AMSSYMB, display = "⊼", collapse = true),
    VEE_BAR("veebar", dependency = AMSSYMB, display = "⊻", collapse = true),
    DOUBLE_BAR_WEDGE("doublebarwedge", dependency = AMSSYMB, display = "⌆", collapse = true),
    CURLY_WEDGE("curlywedge", dependency = AMSSYMB, display = "⋏", collapse = true),
    CURLY_VEE("curlyvee", dependency = AMSSYMB, display = "⋎", collapse = true),
    INTERCALATE("intercal", dependency = AMSSYMB, display = "⊺", collapse = true),
    PITCHFORK("pitchfork", dependency = AMSSYMB, display = "⋔", collapse = true),
    NOT_SIM("nsim", dependency = AMSSYMB),
    SIM_EQUALS("simeq", display = "≃", collapse = true),
    BACKWARDS_SIM_EQUALS("backsimeq", dependency = AMSSYMB, display = "⋍", collapse = true),
    APPROX_EQUALS("approxeq", dependency = AMSSYMB, display = "≊", collapse = true),
    CONG_SYMBOL("cong", dependency = AMSSYMB, display = "≅", collapse = true),
    NOT_CONG("ncong", dependency = AMSSYMB, display = "≇", collapse = true),
    SMILE("smile", dependency = AMSSYMB, display = "\u2323", collapse = true),
    FROWN("frown", dependency = AMSSYMB, display = "\u2322", collapse = true),
    SMALL_SMILE("smallsmile", dependency = AMSSYMB, display = "\u2323", collapse = true),
    SMALL_FROWN("smallfrown", dependency = AMSSYMB, display = "\u2322", collapse = true),
    BETWEEN("between", dependency = AMSSYMB, display = "≬", collapse = true),
    PRECEDES("prec", display = "≺", collapse = true),
    SUCCEEDS("succ", display = "≻", collapse = true),
    NOT_PRECEEDS("nprec", dependency = AMSSYMB, display = "⊀", collapse = true),
    NOT_SUCCEEDS("nsucc", dependency = AMSSYMB, display = "⊁", collapse = true),
    PRECEDES_OR_EQUAL("preceq", dependency = AMSSYMB, display = "⪯", collapse = true),
    SUCCEEDS_OR_EQUALS("succeq", dependency = AMSSYMB, display = "⪰", collapse = true),
    NOT_PRECEDES_OR_EQUALS("npreceq", dependency = AMSSYMB, display = "⋠", collapse = true),
    NOT_SUCCEEDS_OR_EQUALS("nsucceq", dependency = AMSSYMB, display = "⋡", collapse = true),
    CURLY_PRECEDES_OR_EQUALS("preccurlyeq", dependency = AMSSYMB, display = "≼", collapse = true),
    CURLY_SUCCEEDS_OR_EQUALS("succcurlyeq", dependency = AMSSYMB, display = "≽", collapse = true),
    CURLY_EQUALS_PRECEDES("curlyeqprec", dependency = AMSSYMB, display = "⋞", collapse = true),
    CURLY_EQUALS_SUCCEEDS("curlyeqsucc", dependency = AMSSYMB, display = "⋟", collapse = true),
    PRECEDES_SIM("precsim", dependency = AMSSYMB, display = "≾", collapse = true),
    SUCCEEDS_SIM("succsim", dependency = AMSSYMB, display = "≿", collapse = true),
    PRECEDES_NOT_SIM("precnsim", dependency = AMSSYMB, display = "⋨", collapse = true),
    SUCCEEDS_NOT_SIM("succnsim", dependency = AMSSYMB, display = "⋩", collapse = true),
    PRECEDES_APPROX("precapprox", dependency = AMSSYMB, display = "⪷", collapse = true),
    SUCCEEDS_APPROX("succapprox", dependency = AMSSYMB, display = "⪸", collapse = true),
    PRECEDES_NOT_APPROX("precnapprox", dependency = AMSSYMB, display = "⪹", collapse = true),
    SUCCEEDS_NOT_APPROX("succnapprox", dependency = AMSSYMB, display = "⪺", collapse = true),
    PERPENDICULAR("perp", display = "⟂", collapse = true),
    RIGHT_TACK("vdash", display = "⊢", collapse = true),
    NOT_RIGHT_TACK("nvdash", dependency = AMSSYMB, display = "⊬", collapse = true),
    FORCES("Vdash", dependency = AMSSYMB, display = "⊩", collapse = true),
    TRIPLE_RIGHT_TACK("Vvdash", dependency = AMSSYMB, display = "⊪", collapse = true),
    MODELS("models", display = "⊧", collapse = true),
    VERTICAL_DOUBLE_DASH_RIGHT("vDash", dependency = AMSSYMB, display = "⊨", collapse = true),
    NOT_VERTICAL_DOUBLE_DASH_RIGHT("nvDash", dependency = AMSSYMB, display = "⊭", collapse = true),
    NOT_DOUBLE_VERTICAL_DOUBLE_DASH_RIGHT("nVDash", dependency = AMSSYMB, display = "⊯", collapse = true),
    NOT_MID("nmid", dependency = AMSSYMB, display = "∤", collapse = true),
    LESS_THAN_DOT("lessdot", dependency = AMSSYMB, display = "⋖", collapse = true),
    GREATER_THAN_DOT("gtrdot", dependency = AMSSYMB, display = "⋗", collapse = true),
    LESS_THAN_VERTICAL_NOT_EQUALS("lvertneqq", dependency = AMSSYMB),
    GREATER_THAN_VERTICAL_NOT_EQUALS("gvertneqq", dependency = AMSSYMB),
    LESS_THAN_EQUALS_SLANT("leqslant", dependency = AMSSYMB, display = "⩽", collapse = true),
    GREATER_THAN_EQUALS_SLANT("geqslant", dependency = AMSSYMB, display = "⩾", collapse = true),
    NOT_LESS_THAN_EQUALS_SLANT("nleqslant", dependency = AMSSYMB),
    NOT_GREATER_THAN_EQUALS_SLANT("ngeqslant", dependency = AMSSYMB),
    EQUALS_SLANT_LESS_THAN("eqslantless", dependency = AMSSYMB, display = "⪕", collapse = true),
    EQUALS_SLANT_GREATER_THAN("eqslantgtr", dependency = AMSSYMB, display = "⪖", collapse = true),
    LESS_GREATER("lessgtr", dependency = AMSSYMB, display = "≶", collapse = true),
    GREATER_LESS("gtrless", dependency = AMSSYMB, display = "≷", collapse = true),
    LESS_EQUALS_GREATER("lesseqgtr", dependency = AMSSYMB, display = "⋚", collapse = true),
    GREATER_EQUALS_LESSER("gtreqless", dependency = AMSSYMB, display = "⋛", collapse = true),
    LESS_EQUALSS_GREATER("lesseqqgtr", dependency = AMSSYMB, display = "⪋", collapse = true),
    GREATER_EQUALSS_LESSER("gtreqqless", dependency = AMSSYMB, display = "⪌", collapse = true),
    LESS_SIM("lesssim", dependency = AMSSYMB, display = "≲", collapse = true),
    GREATER_SIM("gtrsim", dependency = AMSSYMB, display = "≳", collapse = true),
    LESS_NOT_SIM("lnsim", dependency = AMSSYMB, display = "⋦", collapse = true),
    GREATER_NOT_SIM("gnsim", dependency = AMSSYMB, display = "⋧", collapse = true),
    LESS_APPROX("lessapprox", dependency = AMSSYMB, display = "⪅", collapse = true),
    GREATER_APPROX("gtrapprox", dependency = AMSSYMB, display = "⪆", collapse = true),
    LESS_NOT_APPROX("lnapprox", dependency = AMSSYMB, display = "⪉", collapse = true),
    GREATER_NOT_APPROX("gnapprox", dependency = AMSSYMB, display = "⪊", collapse = true),
    TRIANGLE_RIGHT_VARIATION("vartriangleright", dependency = AMSSYMB, display = "⊳", collapse = true),
    TRIANGLE_LEFT_VARIATION("vartriangleleft", dependency = AMSSYMB, display = "⊲", collapse = true),
    NOT_TRIANGLE_LEFT("ntriangleleft", dependency = AMSSYMB, display = "⋪", collapse = true),
    NOT_TRIANGLE_RIGHT("ntriangleright", dependency = AMSSYMB, display = "⋫", collapse = true),
    TRIANGLE_LEFT_EQUALS("trianglelefteq", dependency = AMSSYMB, display = "⊴", collapse = true),
    TRIANGLE_RIGHT_EQUALS("trianglerighteq", dependency = AMSSYMB, display = "⊵", collapse = true),
    TRIANGLE_LEFT_EQUALS_SLANT("trianglelefteqslant", dependency = STMARYRD),
    TRIANGLE_RIGHT_EQUALS_SLANT("trianglerighteqslant", dependency = STMARYRD),
    NOT_TRIANGLE_LEFT_EQUALS("ntrianglelefteq", dependency = AMSSYMB, display = "⋬", collapse = true),
    NOT_TRIANGLE_RIGHT_EQUALS("ntrianglerighteq", dependency = AMSSYMB, display = "⋭", collapse = true),
    NOT_TRIANGLE_LEFT_EQUALS_SLANT("ntrianglelefteqslant", dependency = STMARYRD),
    NOT_TRIANGLE_RIGHT_SLANT("ntrianglerighteqslant", dependency = STMARYRD),
    BLACK_TRIANGLE_LEFT("blacktriangleleft", dependency = AMSSYMB, display = "◂", collapse = true),
    BLACK_TRIANGLE_RIGHT("blacktriangleright", dependency = AMSSYMB, display = "▸", collapse = true),
    SUBSET_NOT_EQUALS("subsetneq", dependency = AMSSYMB, display = "⊊", collapse = true),
    SUPERSET_NOT_EQUALS("supsetneq", dependency = AMSSYMB, display = "⊋", collapse = true),
    SUBSET_NOT_EQUALS_VARIATION("varsubsetneq", dependency = AMSSYMB),
    SUPERSET_NOT_EQUALS_VARIATION("varsupsetneq", dependency = AMSSYMB),
    SUBSET_NOT_EQUALSS("subsetneqq", dependency = AMSSYMB, display = "⫋", collapse = true),
    SUPERSET_NOT_EQUALSS("supsetneqq", dependency = AMSSYMB, display = "⫌", collapse = true),
    REVERSED_EPSILON("backepsilon", dependency = AMSSYMB, display = "϶", collapse = true),
    DOUBLE_SUBSET("Subset", dependency = AMSSYMB, display = "⋐", collapse = true),
    DOUBLE_SUPERSET("Supset", dependency = AMSSYMB, display = "⋑", collapse = true),
    CIRCLE_EQUALS("circeq", dependency = AMSSYMB, display = "≗", collapse = true),
    TRIANGLE_EQUALS("triangleq", dependency = AMSSYMB, display = "≜", collapse = true),
    EQUALS_CIRCLE("eqcirc", dependency = AMSSYMB),
    BUMP_EQUALS("bumpeq", dependency = AMSSYMB),
    DOUBLE_BUMP_EQUALS("Bumpeq", dependency = AMSSYMB, display = "", collapse = true),
    DOT_EQUALS_DOT("doteqdot", dependency = AMSSYMB, display = "≑", collapse = true),
    RISING_DOTS_EQUALS("risingdotseq", dependency = AMSSYMB, display = "≓", collapse = true),
    FALLING_DOTS_EQUALS("fallingdotseq", dependency = AMSSYMB, display = "≒", collapse = true),
    DOT_EQUALS("doteq", dependency = AMSSYMB),
    SUBSET_PLUS("subsetplus", dependency = STMARYRD),
    SUBSET_PLUS_EQUALS("subsetpluseq", dependency = STMARYRD),
    SUPERSET_PLUS("supsetplus", dependency = STMARYRD),
    SUPERSET_PLUS_EQUALS("supsetpluseq", dependency = STMARYRD),
    IN_PLUS("inplus", dependency = STMARYRD),
    REVERSED_IN_PLUS("niplus", dependency = STMARYRD),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import arrow.core.NonEmptySet
import arrow.core.getOrNone
import arrow.core.nonEmptySetOf

/**
 * @author Hannah Schellekens, Sten Wessel
 */
object LatexRegularCommand {

    private val GENERIC: Set<LatexCommand> = LatexGenericRegularCommand.values().toSet()
    private val TEXTCOMP: Set<LatexCommand> = LatexTextcompCommand.values().toSet()
    private val EURO: Set<LatexCommand> = LatexEuroCommand.values().toSet()
    private val TEXT_SYMBOLS: Set<LatexCommand> = LatexTextSymbolCommand.values().toSet()
    private val NEW_DEFINITIONS: Set<LatexCommand> = LatexNewDefinitionCommand.values().toSet()
    private val MATHTOOLS: Set<LatexCommand> = LatexMathtoolsRegularCommand.values().toSet()
    private val XCOLOR: Set<LatexCommand> = LatexColorDefinitionCommand.values().toSet()
    private val XPARSE: Set<LatexCommand> = LatexXparseCommand.values().toSet()
    private val NATBIB: Set<LatexCommand> = LatexNatbibCommand.values().toSet()
    private val BIBLATEX: Set<LatexCommand> = LatexBiblatexCommand.values().toSet()
    private val SIUNITX: Set<LatexCommand> = LatexSiunitxCommand.values().toSet()
    private val ALGORITHMICX: Set<LatexCommand> = LatexAlgorithmicxCommand.values().toSet()
    private val IFS: Set<LatexCommand> = LatexIfCommand.values().toSet()
    private val LISTINGS: Set<LatexCommand> = LatexListingCommand.values().toSet()
    private val LOREM_IPSUM: Set<LatexCommand> = LatexLoremIpsumCommand.values().toSet()
    private val GLOSSARY: Set<LatexCommand> = LatexGlossariesCommand.values().toSet()

    val ALL: Set<LatexCommand> = GENERIC + TEXTCOMP + EURO + TEXT_SYMBOLS + NEW_DEFINITIONS + MATHTOOLS +
        XCOLOR + XPARSE + NATBIB + BIBLATEX + SIUNITX + ALGORITHMICX + IFS + LISTINGS + LOREM_IPSUM + GLOSSARY

    private val lookup = HashMap<String, NonEmptySet<LatexCommand>>()
    private val lookupDisplay = HashMap<String, NonEmptySet<LatexCommand>>()

    init {
        ALL.forEach { cmd ->
            lookup[cmd.command] = lookup.getOrNone(cmd.command).fold({ nonEmptySetOf(cmd) }, { it + nonEmptySetOf(cmd) })
            if (cmd.display != null) {
                lookupDisplay[cmd.display!!] = lookupDisplay.getOrNone(cmd.display!!).fold({ nonEmptySetOf(cmd) }, { it + nonEmptySetOf(cmd) })
            }
        }
    }

    @JvmStatic
    fun values() = ALL

    @JvmStatic
    operator fun get(command: String) = lookup[command]

    @JvmStatic
    fun findByDisplay(display: String) = lookupDisplay[display]
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.SIUNITX

/**
 * @author Hannah Schellekens
 */
enum class LatexSiunitxCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    ANG("ang", "options".asOptional(), "angle".asRequired(), dependency = SIUNITX),
    NUM("num", "options".asOptional(), "number".asRequired(), dependency = SIUNITX),
    SI("si", "options".asOptional(), "unit".asRequired(), dependency = SIUNITX),
    SI_NUM("SI", "options".asOptional(), "number".asRequired(), "pre-unit".asOptional(), "unit".asRequired(), dependency = SIUNITX),
    NUMLIST("numlist", "options".asOptional(), "numbers".asRequired(), dependency = SIUNITX),
    NUMRANGE("numrange", "options".asOptional(), "number1".asRequired(), "number2".asRequired(), dependency = SIUNITX),
    SILIST("SIlist", "options".asOptional(), "numbers".asRequired(), "unit".asRequired(), dependency = SIUNITX),
    SIRANGE("numrange", "options".asOptional(), "number1".asRequired(), "number2".asRequired(), "unit".asRequired(), dependency = SIUNITX),
    SISETUP("sisetup", "options".asRequired(), dependency = SIUNITX),
    TABLENUM("tablenum", "options".asOptional(), "number".asRequired(), dependency = SIUNITX),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSSYMB
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.MARVOSYM
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.WASYSYM

/**
 * @author Hannah Schellekens
 */
enum class LatexTextSymbolCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    CUT_LEFT_ON_LINE("Cutleft", dependency = MARVOSYM),
    CUT_RIGHT_ON_LINE("Cutright", dependency = MARVOSYM, display = "✁"),
    LEFT_SCISSORS("Leftscissors", dependency = MARVOSYM),
    RIGHT_SCISSORS("Rightscissors", dependency = MARVOSYM, display = "✂"),
    CUT_LINE("Cutline", dependency = MARVOSYM),
    CUT_LINE_MIDDLE("Kutline", dependency = MARVOSYM),
    CHECKMARK("checkmark", dependency = AMSSYMB, display = "✓"),
    MALTESE("maltese", dependency = AMSSYMB, display = "✠"),
    AC("AC", dependency = WASYSYM, display = "∼"),
    TEXT_UPSIDE_DOWN_OMEGA("agemO", dependency = WASYSYM, display = "℧"),
    APL_BOX("APLbox", dependency = WASYSYM, display = "⎕"),
    APL_CIRCLE("APLcirc", dependency = WASYSYM),
    APL_COMMENT("APLcomment", dependency = WASYSYM),
    APL_DOWN("APLdown", dependency = WASYSYM, display = "⍗"),
    APL_DOWN_ARROW_BOX("APLdownarrowbox", dependency = WASYSYM),
    APL_INV("APLinv", dependency = WASYSYM, display = "⌹"),
    APL_INPUT("APLinput", dependency = WASYSYM, display = "⍞"),
    APL_LEFT_ARROW_BOX("APLleftarrowbox", dependency = WASYSYM, display = "⍇"),
    APL_LOG("APLlog", dependency = WASYSYM, display = "⍟"),
    APL_MINUS("APLminus", dependency = WASYSYM, display = "—"),
    APL_NOT("APLnot", dependency = WASYSYM, display = "∼"),
    APL_RIGHT_ARROW_BOX("APLrightarrowbox", dependency = WASYSYM, display = "⍈"),
    APL_STAR("APLstar", dependency = WASYSYM, display = "\uD83D\uDFAF"),
    APL_UP("APLup", dependency = WASYSYM),
    APL_UP_ARROW_BOX("APLuparrowbox", dependency = WASYSYM, display = "⍐"),
    APL_VERTICAL_LINE("APLvert", dependency = WASYSYM, display = "|"),
    APPLECOMMAND("applecmd", dependency = WASYSYM, display = "⌘"),
    AQUARIUS("aquarius", dependency = WASYSYM, display = "♒"),
    ARIES("aries", dependency = WASYSYM, display = "♈"),
    ASCENDING_NODE("ascnode", dependency = WASYSYM, display = "☊"),
    ASTROSUN("astrosun", dependency = WASYSYM, display = "☉"),
    ATARIBOX("ataribox", dependency = WASYSYM),
    BELL("bell", dependency = WASYSYM, display = "\uD83D\uDD14"),
    BLACK_CIRCLE("CIRCLE", dependency = WASYSYM, display = "⏺"),
    BLACK_LEFT_CIRCLE("LEFTCIRCLE", dependency = WASYSYM, display = "◖"),
    BLACK_LEFT_CIRCLE_2("LEFTcircle", dependency = WASYSYM, display = "◖"),
    BLACK_RIGHT_CIRCLE("RIGHTCIRCLE", dependency = WASYSYM, display = "◗"),
    BLACK_RIGHT_CIRCLE_2("RIGHTcircle", dependency = WASYSYM, display = "◗"),
    BLACK_SMILEY("blacksmiley", dependency = WASYSYM, display = "☻"),
    TEXT_BOWTIE("Bowtie", dependency = WASYSYM, display = "⋈"),
    BROKENVERT("brokenvert", dependency = WASYSYM, display = "¦"),
    CANCER("cancer", dependency = WASYSYM, display = "♋"),
    WHITE_CIRCLE("Circle", dependency = WASYSYM, display = "○"),
    EMPTY_BOX("Square", dependency = WASYSYM, display = "□"),
    CAPITAL_THORN("Thorn", dependency = WASYSYM, display = "þ"),
    CAPRICORNUS("capricornus", dependency = WASYSYM, display = "♑"),
    CENT("cent", dependency = WASYSYM, display = "¢"),
    CHECKED("checked", dependency = WASYSYM, display = "✓"),
    CHECKED_BOX("CheckedBox", dependency = WASYSYM, display = "☑"),
    CLOCK("clock", dependency = WASYSYM, display = "⏲"),
    CONJUNCTION("conjunction", dependency = WASYSYM, display = "☌"),
    CROSSED_BOX("XBox", dependency = WASYSYM, display = "☒"),
    CURRENCY("currency", dependency = WASYSYM, display = "¤"),
    STAR_OF_DAVID("davidsstar", dependency = WASYSYM, display = "✡"),
    DESCENDING_NODE("descnode", dependency = WASYSYM, display = "☋"),
    DIAMETER("diameter", dependency = WASYSYM, display = "⌀"),
    DOWN_TRIANGLE("DOWNarrow", dependency = WASYSYM, display = "▼"),
    EARTH("earth", dependency = WASYSYM, display = "♁"),
    TEXT_EIGTH_NOTE("eighthnote", dependency = WASYSYM, display = "♪"),
    FEMALE("female", dependency = WASYSYM, display = "♀"),
    FROWNIE("frownie", dependency = WASYSYM, display = "☹"),
    FULL_MOON("fullmoon", dependency = WASYSYM, display = "○"),
    FULLNOTE("fullnote", dependency = WASYSYM, display = "\uD834\uDD5D"),
    GEMINI("gemini", dependency = WASYSYM, display = "♊"),
    GLUON("gluon", dependency = WASYSYM),
    TEXT_HALF_NOTE("halfnote", dependency = WASYSYM, display = "\uD834\uDD5E"),
    HEXAGON_LYING("hexagon", dependency = WASYSYM),
    SIX_LEGGED_STAR_LYING("hexstar", dependency = WASYSYM),
    HF("HF", dependency = WASYSYM, display = "∼"),
    DIAMETER_MIRRORED("invdiameter", dependency = WASYSYM),
    INVE("inve", dependency = WASYSYM, display = "Ə"),
    JUPITER("jupiter", dependency = WASYSYM, display = "♃"),
    MALTESE_CROSS("kreuz", dependency = WASYSYM, display = "✠"),
    LEFT_MOON("leftmoon", dependency = WASYSYM, display = "☾"),
    TEXT_ANTI_CLOCKWISE_ROUND_ARROW("leftturn", dependency = WASYSYM, display = "↺"),
    WHITE_LEFT_CIRCLE("Leftcircle", dependency = WASYSYM),
    LEFT_TRIANGLE("LEFTarrow", dependency = WASYSYM, display = "⯇"),
    LEO("leo", dependency = WASYSYM, display = "♌"),
    LIBRA("libra", dependency = WASYSYM, display = "♎"),
    LIGHTNING_WASYSYM("lightning", dependency = WASYSYM, display = "☇"),
    MALE("male", dependency = WASYSYM, display = "♂"),
    MARS("mars", dependency = WASYSYM, display = "♂"),
    MERCURY("mercury", dependency = WASYSYM, display = "☿"),
    NEPTUNE("neptune", dependency = WASYSYM, display = "♆"),
    NEW_MOON("newmoon", dependency = WASYSYM, display = "◯"),
    NOT_BACKSLASH("notbackslash", dependency = WASYSYM),
    NOT_SLASH("notslash", dependency = WASYSYM),
    OCTAGON("octagon", dependency = WASYSYM),
    OPENO("openo", dependency = WASYSYM, display = "ᵓ"),
    OPPOSITION("opposition", dependency = WASYSYM, display = "☍"),
    PENTAGON("pentagon", dependency = WASYSYM, display = "⬠"),
    PERMIL("permil", dependency = WASYSYM, display = "‰"),
    PHONE("phone", dependency = WASYSYM, display = "☏"),
    PHOTON("photon", dependency = WASYSYM, display = "〜〜〜"),
    PISCES("pisces", dependency = WASYSYM, display = "♓"),
    PLUTO("pluto", dependency = WASYSYM, display = "♇"),
    POINTER("pointer", dependency = WASYSYM, display = "⇨"),
    TEXT_QUARTER_NOTE("quarternote", dependency = WASYSYM, display = ""),
    RECORDER("recorder", dependency = WASYSYM, display = "♩"),
    RIGHT_MOON("rightmoon", dependency = WASYSYM, display = "☽"),
    TEXT_CLOCKWISE_ROUND_ARROW("rightturn", dependency = WASYSYM, display = "↻"),
    WHITE_RIGHT_CIRCLE("Rightcircle", dependency = WASYSYM),
    RIGHT_TRIANGLE("RIGHTarrow", dependency = WASYSYM, display = "⯈"),
    SAGITTARIUS("sagittarius", dependency = WASYSYM, display = "♐"),
    SATURN("saturn", dependency = WASYSYM, display = "♄"),
    SCORPIO("scorpio", dependency = WASYSYM, display = "♏"),
    SMILEY("smiley", dependency = WASYSYM, display = "☺"),
    SUN("sun", dependency = WASYSYM, display = "☼"),
    TAURUS("taurus", dependency = WASYSYM, display = "♉"),
    THORN("thorn", dependency = WASYSYM, display = "þ"),
    TWONOTES("twonotes", dependency = WASYSYM, display = "♫"),
    UP_TRIANGLE("UParrow", dependency = WASYSYM, display = "▲"),
    URANUS("uranus", dependency = WASYSYM, display = "⛢"),
    MEASURED_ANGLE_WASYSYM("varangle", dependency = WASYSYM, display = "∡"),
    HEXAGON_STANDING("varhexagon", dependency = WASYSYM, display = "⬡"),
    SIX_LEGGED_STAR_STANDING("varhexstar", dependency = WASYSYM, display = "\uD83D\uDFB5"),
    VENUS("venus", dependency = WASYSYM, display = "♀"),
    VERNAL("vernal", dependency = WASYSYM, display = "♈"),
    VHF("VHF", dependency = WASYSYM, display = "≋"),
    VIRGO("virgo", dependency = WASYSYM, display = "♍"),
    WASYCOMMAND("wasycmd", dependency = WASYSYM, display = "⌘"),
    LOZENGE_WASYSYM("wasylozenge", dependency = WASYSYM, display = "⯏"),
    WASYTHEREFORE("wasytherefore", dependency = WASYSYM, display = "∴"),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.TEXTCOMP

/**
 * @author Hannah Schellekens
 */
enum class LatexTextcompCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    TEXT_TILDE_LOW("texttildelow", dependency = TEXTCOMP, display = "˷"),
    TEXTASCIIMACRON("textasciimacron", dependency = TEXTCOMP, display = "¯"),
    TEXT_ASCII_ACUTE("textasciiacute", dependency = TEXTCOMP, display = "´"),
    TEXT_ASCII_DIERESIS("textasciidieresis", dependency = TEXTCOMP, display = "¨"),
    TEXT_ASCII_GRAVE("textasciigrave", dependency = TEXTCOMP, display = "`"),
    TEXT_ASCII_BREVE("textasciibreve", dependency = TEXTCOMP, display = "˘"),
    TEXT_ASCII_CARON("textasciicaron", dependency = TEXTCOMP, display = "˘"),
    TEXT_DOUBLE_ACUTE("textacutedbl", dependency = TEXTCOMP, display = "̋"),
    TEXT_DOUBLE_GRAVE("textgravedbl", dependency = TEXTCOMP, display = "̏"),
    TEXT_QUOTE_STRAIGHT_BASE("textquotestraightbase", dependency = TEXTCOMP, display = "‚"),
    TEXT_DOUBLE_QUOTE_STRAIGHT_BASE("textquotestraightdblbase", dependency = TEXTCOMP, display = "„"),
    TEXT_QUOTE_SINGLE("textquotesingle", dependency = TEXTCOMP, display = "‛"),
    TEXT_DOUBLE_HYPHEN("textdblhyphen", dependency = TEXTCOMP),
    TEXT_DOUBLE_HYPHEN_CHARACTER("textdblhyphenchar", dependency = TEXTCOMP),
    TEXTASTERISKCENTERED_TEXTCOMP("textasteriskcentered", dependency = TEXTCOMP, display = "∗"),
    TEXT_INTERROBANG("textinterrobang", dependency = TEXTCOMP, display = "‽"),
    TEXT_UPSIDE_DOWN_INTERROBANG("textinterrobangdown", dependency = TEXTCOMP),
    TEXTTWELVEUDASH("texttwelveudash", dependency = TEXTCOMP, display = "—"),
    TEXT_THREE_QUARTERS_EM_DASH("textthreequartersemdash", dependency = TEXTCOMP, display = "—"),
    TEXT_CURRENCY("textcurrency", dependency = TEXTCOMP, display = "¤"),
    TEXT_BAHT("textbaht", dependency = TEXTCOMP, display = "฿"),
    TEXT_GUARANI("textguarani", dependency = TEXTCOMP, display = "₲"),
    TEXT_WON("textwon", dependency = TEXTCOMP, display = "₩"),
    TEXT_CENT("textcent", dependency = TEXTCOMP, display = "¢"),
    TEXT_CENT_OLD_STYLE("textcentoldstyle", dependency = TEXTCOMP),
    TEXT_DOLLAR_OLD_STYLE("textdollaroldstyle", dependency = TEXTCOMP),
    TEXT_LIRA("textlira", dependency = TEXTCOMP, display = "₤"),
    TEXT_YEN("textyen", dependency = TEXTCOMP, display = "¥"),
    TEXT_DONG("textdong", dependency = TEXTCOMP, display = "₫"),
    TEXT_NAIRA("textnaira", dependency = TEXTCOMP, display = "₦"),
    TEXT_COLON_MONETARY("textcolonmonetary", dependency = TEXTCOMP, display = "₡"),
    TEXT_PESO("textpeso", dependency = TEXTCOMP, display = "₱"),
    TEXT_FLORIN("textflorin", dependency = TEXTCOMP, display = "ƒ"),
    TEXT_EURO("texteuro", dependency = TEXTCOMP, display = "€"),
    TEXT_COPYLEFT("textcopyleft", dependency = TEXTCOMP, display = "\uD83C\uDD2F"),
    TEXT_SERVICE_MARK("textservicemark", dependency = TEXTCOMP, display = "℠"),
    TEXT_ONE_HALF("textonehalf", dependency = TEXTCOMP, display = "½"),
    TEXT_ONE_QUARTER("textonequarter", dependency = TEXTCOMP, display = "¼"),
    TEXT_THREE_QUARTERS("textthreequarters", dependency = TEXTCOMP, display = "¾"),
    TEXT_ONE_SUPERIOR("textonesuperior", dependency = TEXTCOMP, display = "¹"),
    TEXT_TWO_SUPERIOR("texttwosuperior", dependency = TEXTCOMP, display = "²"),
    TEXT_THREE_SUPERIOR("textthreesuperior", dependency = TEXTCOMP, display = "³"),
    TEXT_NUMERO("textnumero", dependency = TEXTCOMP, display = "№"),
    TEXT_PER_TEN_THOUSAND("textpertenthousand", dependency = TEXTCOMP, display = "‱"),
    TEXT_PER_MILLE("textperthousand", dependency = TEXTCOMP, display = "‰"),
    TEXT_DISCOUNT("textdiscount", dependency = TEXTCOMP, display = "⁒"),
    TEXT_BLANK("textblank", dependency = TEXTCOMP, display = "␢"),
    TEXT_RECIPE("textrecipe", dependency = TEXTCOMP, display = "℞"),
    TEXT_ESTIMATED("textestimated", dependency = TEXTCOMP, display = "℮"),
    TEXT_REFERENCE_MARK("textreferencemark", dependency = TEXTCOMP, display = "※"),
    TEXT_MUSICAL_QUARTER_NOTE("textmusicalnote", dependency = TEXTCOMP, display = "♪"),
    TEXT_PILCROW("textpilcrow", dependency = TEXTCOMP, display = "¶"),
    TEXT_BIG_CIRCLE("textbigcircle", dependency = TEXTCOMP, display = "○"),
    TEXT_OPEN_BULLET("textopenbullet", dependency = TEXTCOMP, display = "◦"),
    TEXT_ORDINAL_INDICATOR_MASCULINE("textordmasculine", dependency = TEXTCOMP, display = "º"),
    TEXT_ORDINAL_INDICATOR_FEMININE("textordfeminine", dependency = TEXTCOMP, display = "ª"),
    TEXT_BORN("textborn", dependency = TEXTCOMP, display = "★"),
    TEXT_DIVORCED("textdivorced", dependency = TEXTCOMP, display = "⚮"),
    TEXT_DIED("textdied", dependency = TEXTCOMP, display = "✝"),
    TEXT_MARRIED("textmarried", dependency = TEXTCOMP, display = "⚭"),
    TEXT_LEAF("textleaf", dependency = TEXTCOMP),
    TEXT_CELCIUS("textcelsius", dependency = TEXTCOMP, display = "℃"),
    TEXT_DEGREE("textdegree", dependency = TEXTCOMP, display = "°"),
    TEXT_UPSIDE_DOWN_OHM("textmho", dependency = TEXTCOMP, display = "℧"),
    TEXT_OHM("textohm", dependency = TEXTCOMP, display = "Ω"),
    TEXT_BROKEN_BAR("textbrokenbar", dependency = TEXTCOMP, display = "¦"),
    TEXT_DOUBLE_BAR("textbardbl", dependency = TEXTCOMP, display = "‖"),
    TEXT_FRACTION_SOLIDUS("textfractionsolidus", dependency = TEXTCOMP, display = "⁄"),
    TEXT_LEFT_ANGLE_BRACKET("textlangle", dependency = TEXTCOMP, display = "〈"),
    TEXT_RIGHT_ANGLE_BRACKET("textrangle", dependency = TEXTCOMP, display = "〉"),
    TEXT_LEFT_NOT("textlnot", dependency = TEXTCOMP, display = "¬"),
    TEXT_MINUS("textminus", dependency = TEXTCOMP, display = "−"),
    TEXT_LEFT_DOUBLE_BRACKET("textlbrackdbl", dependency = TEXTCOMP, display = "〚"),
    TEXT_RIGHT_DOUBLE_BRACKET("textrbrackdbl", dependency = TEXTCOMP, display = "〛"),
    TEXT_MU("textmu", dependency = TEXTCOMP, display = "μ"),
    TEXT_PLUS_MINUS("textpm", dependency = TEXTCOMP, display = "±"),
    TEXT_LEFT_QUILL("textlquill", dependency = TEXTCOMP, display = "⁅"),
    TEXT_RIGHT_QUILL("textrquill", dependency = TEXTCOMP, display = "⁆"),
    TEXT_SURD("textsurd", dependency = TEXTCOMP, display = "√"),
    TEXT_TIMES("texttimes", dependency = TEXTCOMP, display = "×"),
    TEXT_DIVIDE("textdiv", dependency = TEXTCOMP, display = "÷"),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.STMARYRD

/**
 * @author Hannah Schellekens
 */
enum class LatexUncategorizedStmaryrdSymbols(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = true,
    val collapse: Boolean = false
) : LatexCommand {

    Y_LEFT("Yleft", dependency = STMARYRD),
    Y_UP("Yup", dependency = STMARYRD),
    Y_RIGHT("Yright", dependency = STMARYRD),
    Y_DOWN("Ydown", dependency = STMARYRD),
    BIN_AMPERSAND("binampersand", dependency = STMARYRD, display = "&", collapse = true),
    BIN_REVERSED_AMPERSAND("bindnasrepma", dependency = STMARYRD),
    O_BAR("baro", dependency = STMARYRD, display = "⏀", collapse = true),
    BOX_BAR("boxbar", dependency = STMARYRD),
    BOX_CIRCLE("boxcircle", dependency = STMARYRD),
    BOX_BOX("boxbox", dependency = STMARYRD),
    BOXED_DOT_STMARYRD("boxdot", dependency = STMARYRD),
    BOX_ASTERISK("boxast", dependency = STMARYRD),
    BOX_BSLASH("boxbslash", dependency = STMARYRD),
    BOX_EMPTY("boxempty", dependency = STMARYRD, display = "◻", collapse = true),
    BOX_SLASH("boxslash", dependency = STMARYRD),
    BIG_BOX("bigbox", dependency = STMARYRD),
    CURLY_VEE_VARIANT("varcurlyvee", dependency = STMARYRD),
    BIG_CURLY_VEE("bigcurlyvee", dependency = STMARYRD),
    CURLY_WEDGE_VARIANT("varcurlywedge", dependency = STMARYRD),
    BIG_CURLY_WEDGE_VARIANT("bigcurlywedge", dependency = STMARYRD),
    CURLY_VEE_DOWN_ARROW("curlyveedownarrow", dependency = STMARYRD),
    CURLY_VEE_UP_ARROW("curlyveeuparrow", dependency = STMARYRD),
    CURLY_WEDGE_DOWN_ARROW("curlywedgedownarrow", dependency = STMARYRD),
    CURLY_WEDGE_UP_ARROW("curlywedgeuparrow", dependency = STMARYRD),
    BLACKBOARD_BACKSLASH("bbslash", dependency = STMARYRD, display = "⑊", collapse = true),
    BLACKBOARD_SLASH("sslash", dependency = STMARYRD, display = "⃫", collapse = true),
    FAT_BACKSLASH("fatbslash", dependency = STMARYRD),
    FAT_SLASH("fatslash", dependency = STMARYRD),
    FAT_SEMICOLON("fatsemi", dependency = STMARYRD),
    BIG_PARALLEL("bigparallel", dependency = STMARYRD, display = "∥", collapse = true),
    INTERLEAVE("interleave", dependency = STMARYRD, display = "⫴", collapse = true),
    BIG_INTERLEAVE("biginterleave", dependency = STMARYRD, display = "⫴", collapse = true),
    TALL_OBLONG("talloblong", dependency = STMARYRD, display = "▯", collapse = true),
    LEFT_SLICE("leftslice", dependency = STMARYRD),
    RIGHT_SLICE("rightslice", dependency = STMARYRD),
    MERGE("merge", dependency = STMARYRD),
    MOO("moo", dependency = STMARYRD),
    CAP_PLUS("nplus", dependency = STMARYRD),
    BIG_CAP_PLUS("bignplus", dependency = STMARYRD),
    O_HORIZONTAL_DASH("minuso", dependency = STMARYRD, display = "⦵", collapse = true),
    O_WITH_VERTICAL_LINE("obar", dependency = STMARYRD, display = "⊖", collapse = true),
    OBLONG("oblong", dependency = STMARYRD, display = "▯", collapse = true),
    O_WITH_BACKSLASH("obslash", dependency = STMARYRD, display = "⃠", collapse = true),
    O_WITH_GREATER_THAN("ogreaterthan", dependency = STMARYRD, display = "⧁", collapse = true),
    O_WITH_LESS_THAN("olessthan", dependency = STMARYRD, display = "⧀", collapse = true),
    O_WITH_VEE("ovee", dependency = STMARYRD),
    O_WITH_WEDGE("owedge", dependency = STMARYRD),
    BIG_CIRCLE_VARIANT("varbigcirc", dependency = STMARYRD, display = "◯", collapse = true),
    O_WITH_STAR_VARIANT("varoast", dependency = STMARYRD, display = "⊛", collapse = true),
    O_WITH_VERTICAL_BAR_VARIANT("varobar", dependency = STMARYRD, display = "⦶", collapse = true),
    O_WITH_BACKSLASH_VARIANT("varobslash", dependency = STMARYRD, display = "⦸", collapse = true),
    O_WITH_CIRCLE_VARIANT("varocircle", dependency = STMARYRD, display = "⦾", collapse = true),
    O_WITH_DOT_VARIANT("varodot", dependency = STMARYRD, display = "⨀", collapse = true),
    O_WITH_GREATER_THAN_VARIANT("varogreaterthan", dependency = STMARYRD, display = "⧁", collapse = true),
    O_WITH_LESS_THAN_VARIANT("varolessthan", dependency = STMARYRD, display = "⧀", collapse = true),
    O_WITH_MINUS_VARIANT("varominus", dependency = STMARYRD, display = "⊖", collapse = true),
    O_WITH_PLUS_VARIANT("varoplus", dependency = STMARYRD, display = "⊕", collapse = true),
    O_WITH_SLASH_VARIANT("varoslash", dependency = STMARYRD, display = "⊘", collapse = true),
    O_WITH_TIMES_VARIANT("varotimes", dependency = STMARYRD, display = "⊗", collapse = true),
    O_WITH_VEE_VARIANT("varovee", dependency = STMARYRD),
    O_WITH_WEDGE_VARIANT("varowedge", dependency = STMARYRD),
    TIMES_VARIANT("vartimes", dependency = STMARYRD, display = "⨉", collapse = true),
    BIG_SQUARE_CAP("bigsqcap", dependency = STMARYRD, display = "⊓", collapse = true),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.lang.LatexPackage

/**
 * The xparse package is included in the kernel since the 2020/10/1 release.
 *
 * @author Hannah Schellekens
 */
enum class LatexXparseCommand(
    override val command: String,
    override vararg val arguments: Argument = emptyArray(),
    override val dependency: LatexPackage = LatexPackage.DEFAULT,
    override val display: String? = null,
    override val isMathMode: Boolean = false,
    val collapse: Boolean = false
) : LatexCommand {

    NEWDOCUMENTCOMMAND("NewDocumentCommand", "name".asRequired(), "args spec".asRequired(), "code".asRequired()),
    RENEWDOCUMENTCOMMAND("RenewDocumentCommand", "name".asRequired(), "args spec".asRequired(), "code".asRequired()),
    PROVIDEDOCUMENTCOMMAND("ProvideDocumentCommand", "name".asRequired(), "args spec".asRequired(), "code".asRequired()),
    DECLAREDOCUMENTCOMMAND("DeclareDocumentCommand", "name".asRequired(), "args spec".asRequired(), "code".asRequired()),
    NEWDOCUMENTENVIRONMENT("NewDocumentEnvironment", "name".asRequired(), "args spec".asRequired(), "start code".asRequired(), "end code".asRequired()),
    RENEWDOCUMENTENVIRONMENT("RenewDocumentEnvironment", "name".asRequired(), "args spec".asRequired(), "start code".asRequired(), "end code".asRequired()),
    PROVIDEDOCUMENTENVIRONMENT("ProvideDocumentEnvironment", "name".asRequired(), "args spec".asRequired(), "start code".asRequired(), "end code".asRequired()),
    DECLAREDOCUMENTENVIRONMENT("DeclareDocumentEnvironment", "name".asRequired(), "args spec".asRequired(), "start code".asRequired(), "end code".asRequired()),
    ;

    override val identifier: String
        get() = name
}

package nl.hannahsten.texifyidea.lang.commands

/**
 * @author Sten Wessel
 */
@Suppress("ConvertSecondaryConstructorToPrimary")
class OptionalArgument : Argument {

    @JvmOverloads
    internal constructor(name: String, type: Type = Type.NORMAL) : super(name, type)

    override fun toString() = "[$name]"

    override fun equals(other: Any?): Boolean {
        return (other as? OptionalArgument)?.name == this.name
    }

    override fun hashCode(): Int {
        return this.name.hashCode()
    }
}

package nl.hannahsten.texifyidea.lang.commands

/**
 * @author Sten Wessel
 */
@Suppress("ConvertSecondaryConstructorToPrimary")
open class RequiredArgument : Argument {

    @JvmOverloads
    internal constructor(name: String, type: Type = Type.NORMAL) : super(name, type)

    override fun toString() = "{$name}"

    override fun equals(other: Any?) = (other as? RequiredArgument)?.name == this.name

    override fun hashCode() = this.name.hashCode()
}

package nl.hannahsten.texifyidea.lang.commands

import nl.hannahsten.texifyidea.file.FileExtensionMatcher
import nl.hannahsten.texifyidea.file.FileNameMatcher
import java.util.*
import java.util.regex.Pattern

/**
 * A required file argument with a given name and a pattern that matches
 * corresponding file names.
 * Ignores case: everything will be converted to lower case.
 * Type will be [nl.hannahsten.texifyidea.lang.commands.Argument.Type.NORMAL].
 *
 * @param name
 * The name of the required argument.
 * @param commaSeparatesArguments True if arguments are separated by commas, for example \command{arg1,arg2}. If false, "arg1,arg2" will be seen as one argument.
 * @param extensions
 * All supported extensions, of which the first extension is the default extension.
 * @author Hannah Schellekens
 */
open class RequiredFileArgument(name: String?, open val isAbsolutePathSupported: Boolean = true, open val commaSeparatesArguments: Boolean, vararg extensions: String) : RequiredArgument(name!!, Type.FILE), FileNameMatcher, FileExtensionMatcher {

    lateinit var supportedExtensions: Set<String>
    lateinit var defaultExtension: String
        private set
    private var pattern: Pattern? = null

    init {
        setExtensions(*extensions)
    }

    /**
     * Registers all extensions and compiles them to a regex file matcher.
     *
     * @param extensions
     * All the file extensions (lower case) that should result in match for this required
     * argument. The file extensions should *not* have a dot. When given no
     * extensions, all files will match.
     */
    private fun setExtensions(vararg extensions: String) {
        val supportedExtensions = mutableSetOf<String>()
        val regex = StringBuilder(".*")
        if (extensions.isEmpty()) {
            setRegex(regex.toString())
            this.supportedExtensions = supportedExtensions
            this.defaultExtension = ""
            return
        }
        else {
            defaultExtension = extensions[0]
        }
        regex.append("(")
        for (extension in extensions) {
            regex.append("\\.")
            val extensionLower = extension.lowercase(Locale.getDefault())
            regex.append(extensionLower)
            supportedExtensions.add(extensionLower)
            if (extension != extensions[extensions.size - 1]) {
                regex.append("|")
            }
        }
        regex.append(")$")
        setRegex(regex.toString())

        this.supportedExtensions = supportedExtensions
    }

    private fun setRegex(regex: String) {
        pattern = Pattern.compile(regex)
    }

    override fun matchesName(fileName: String): Boolean {
        return pattern!!.matcher(fileName.lowercase(Locale.getDefault())).matches()
    }

    override fun matchesExtension(extension: String): Boolean {
        return supportedExtensions.contains(extension)
    }
}

package nl.hannahsten.texifyidea.lang.commands

/**
 * @author Lukas Heiligenbrunner
 */
class RequiredFolderArgument(name: String?) : RequiredArgument(name!!, Type.FILE)

package nl.hannahsten.texifyidea.lang.commands

/**
 * @author Lukas Heiligenbrunner
 */
// We have to explicitly override isAbsolutePathSupported to avoid a NoSuchMethodError
class RequiredPicturePathArgument(name: String, override val isAbsolutePathSupported: Boolean = true, override val commaSeparatesArguments: Boolean = true, vararg extension: String) : RequiredFileArgument(name, isAbsolutePathSupported, commaSeparatesArguments, *extension)

package nl.hannahsten.texifyidea.lang.graphic

import nl.hannahsten.texifyidea.lang.Described

/**
 * Where to put the caption inside a figure environment.
 *
 * @author Hannah Schellekens
 */
enum class CaptionLocation(override val description: String) : Described {

    ABOVE_GRAPHIC("above graphic"),
    BELOW_GRAPHIC("below graphic");

    override fun toString() = description
}

package nl.hannahsten.texifyidea.lang.graphic

import nl.hannahsten.texifyidea.lang.Described
import nl.hannahsten.texifyidea.lang.LatexPackage

/**
 * @author Hannah Schellekens
 */
enum class FigureLocation(
    val symbol: String,
    override val description: String,
    val requiredPackage: LatexPackage? = null
) : Described {

    TOP("t", "top"),
    BOTTOM("b", "bottom"),
    PAGE("p", "page"),
    HERE("h", "here"),
    STRICT_HERE("H", "strict here", requiredPackage = LatexPackage.FLOAT),
    OVERRIDE_INTERNAL_PARAMETERS("!", "override");

    override fun toString() = description

    companion object {

        val ALL_SYMBOLS = values().mapNotNull { it.symbol.firstOrNull() }

        fun bySymbol(symbol: String) = values().firstOrNull { it.symbol == symbol }
    }
}

package nl.hannahsten.texifyidea.lang.magic

/**
 * @author Hannah Schellekens
 */
open class CustomMagicKey<Key>(override val key: Key) : MagicKey<Key> {

    override fun toString() = key.toString()

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is CustomMagicKey<*>) return false
        return key == other.key
    }

    override fun hashCode(): Int {
        return key?.hashCode() ?: 0
    }
}

package nl.hannahsten.texifyidea.lang.magic

import nl.hannahsten.texifyidea.run.compiler.BibliographyCompiler
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler

/**
 * @author Hannah Schellekens
 */
enum class DefaultMagicKeys(
    override val key: String,
    val displayKey: String,
    override val documentation: String,
    override val targets: Set<MagicCommentScope> = MagicCommentScope.ALL_SCOPES
) : MagicKey<String> {

    // TeXworks-esque relevant keys.
    COMPILER(
        "compiler",
        "Compiler",
        """
        The name of the typesetting engine to use for the current file.
        The following programs are supported:
        ${LatexCompiler.values().joinToString(", ") { it.executableName }}
        """.trimIndent().trim(),
        MagicCommentScope.FILE.singleScope()
    ),

    PROGRAM(
        "program",
        "Program",
        """
        The name of the typesetting engine to use for the current file.
        The following programs are supported:
        ${LatexCompiler.values().joinToString(", ") { it.executableName }}
        """.trimIndent().trim(),
        MagicCommentScope.FILE.singleScope()
    ),

    BIBTEXCOMPILER(
        "bibtex compiler",
        "BibTeX Compiler",
        """
        The name of the typesetting engine to use for the current file.
        The following programs are supported:
        ${BibliographyCompiler.values()
            .joinToString(", ") { it.executableName }}
        """.trimIndent().trim(),
        MagicCommentScope.FILE.singleScope()
    ),

    ROOT(
        "root",
        "Root",
        """
        Indicates that the file with the given name (relative to this file) is the root file of your document.
        """.trimIndent().trim(),
        MagicCommentScope.FILE.singleScope()
    ),

    // TeXiFy functionality.
    SUPPRESS(
        "suppress",
        "Suppress",
        """
        Denotes that an inspection in the following scope must be suppressed.
        A value of 'All' means that all inspections will be suppressed.
        You can suppress multiple inspections by separating the name of inspections by a comma.
        """.trimIndent().trim()
    ),

    INJECT_LANGUAGE(
        "language",
        "Language",
        """
        Indicates that a language should be injected in the following environment.
        """.trimIndent().trim(),
        MagicCommentScope.ENVIRONMENT.singleScope()
    ),

    BEGIN_PREAMBLE(
        "begin preamble",
        "Begin preamble",
        """
        Indicates the start of a (part of) preamble to be included in the preview.
        """.trimIndent().trim(),
        MagicCommentScope.FILE.singleScope()
    ),

    END_PREAMBLE(
        "end preamble",
        "End preamble",
        """
        Indicates the end of a (part of) preamble to be included in the preview.
        """.trimIndent().trim(),
        MagicCommentScope.FILE.singleScope()
    ),

    PREVIEW_PREAMBLE(
        "preview preamble",
        "Preview preamble",
        """
        Indicates that this file has to be included in the preamble of the preview.
        """.trimIndent().trim(),
        MagicCommentScope.FILE.singleScope()
    ),

    PARSER(
        "parser",
        "Parser",
        """
        Allows for switching parsing off and on.
        """.trimIndent().trim(),
        MagicCommentScope.FILE.singleScope()
    ),

    // TeXdoc.
    INFO(
        "info",
        "Info",
        """
        A documentation description for a definition.
        """.trimIndent().trim(),
        MagicCommentScope.COMMAND.singleScope()
    ),

    PARAM(
        "param",
        "Param",
        """
        Adds documentation for the next required parameter.
        The first usage of 'Param' will document the 1st parameter, the second usage of 'Param' the 2nd etc.
        """.trimIndent().trim(),
        MagicCommentScope.COMMAND.singleScope()
    ),

    OPTIONAL(
        "optional",
        "Optional",
        """
        Adds documentation for the next optional parameter.
        The first usage of 'Optional' will document the 1st parameter, the second usage of 'Optional' the 2nd etc.
        The value starts with the format <tt>fieldname=defaultvalue</tt>.
        After a separating space the documentation description starts.
        """.trimIndent().trim(),
        MagicCommentScope.COMMAND.singleScope()
    ),

    SINCE(
        "since",
        "Since",
        """
        The version number that denotes since when the feature is available.
        """.trimIndent().trim(),
        MagicCommentScope.COMMAND.singleScope()
    ),

    AUTHOR(
        "author",
        "Author",
        """
        The name of the author.
        """.trimIndent().trim(),
        MagicCommentScope.COMMAND.singleScope()
    ),

    SEE(
        "see",
        "See",
        """
        A comma separated list of command names (including backslash) and environments to link to in the documentation.
        These commands and environments are related to the documented command or environment.
        """.trimIndent().trim(),
        MagicCommentScope.COMMAND.singleScope()
    ),

    FAKE(
        "fake",
        "Fake",
        """
        A comment tdieksoe
        """.trimIndent().trim(),
        MagicCommentScope.COMMAND.singleScope()
    );

    override fun toString(): String = key
}

package nl.hannahsten.texifyidea.lang.magic

/**
 * A magic comment is a special comment that provides meta data about the document to the IDE.
 *
 * They store values for each key defined in a magic comment.
 * A single key can have multiple values: when the same key is used multiple times.
 * The values will be stored in order.
 * When the key has no value (i.e. the key is mentioned standalone in a comment without an assignment)
 * the list will be empty.
 * When a key is not present in the magic comment, the store does not contain the key.
 *
 * Note that keys are unique, values are not.
 *
 * @param Key
 *          The type of key to use.
 * @param Value
 *          The type of the values.
 *
 * @author Hannah Schellekens
 */
abstract class MagicComment<Key, Value> {

    companion object {

        /**
         * Creates a new empty magic comment.
         */
        fun <Key, Value> empty(): MagicComment<Key, Value> = MutableMagicComment()
    }

    /**
     * Maps all keys in the magic comment to their values.
     *
     * @see [MagicComment]
     */
    protected val keyValueStore: MutableMap<Key, MutableList<Value>> = HashMap()

    /**
     * Get all the keys in the key value store.
     */
    fun keySet(): Set<Key> = keyValueStore.keys.toSet()

    /**
     * Checks if a key-value pair with the given key is present in the magic comment.
     *
     * @return `true` if present, `false` if absent.
     */
    fun containsKey(key: MagicKey<Key>): Boolean = keyValueStore.containsKey(key.key)

    /**
     * Get the value of the first key-value pair with the given key.
     *
     * @return The value of the first key-value pair, or `null` when the key is absent, or when there are no values
     * available for the given key.
     */
    fun value(key: MagicKey<Key>): Value? = keyValueStore[key.key]?.firstOrNull()

    /**
     * Get all the values of the key-value pairs with the given key.
     *
     * @return The list of all stored values for the given key (empty list if the key is registered without a value),
     * or `null` when the key is absent.
     */
    fun values(key: MagicKey<Key>): List<Value>? = keyValueStore[key.key]

    /**
     * The total number of keys in the magic comment.
     */
    fun size() = keyValueStore.size

    /**
     * Combines the keys and their values of both comments in a new magic comment.
     * If both comments share the same value for the same key, the value will appear multiple times.
     * The value order is: values from `this` before values from `mergeWith`.
     */
    abstract fun merge(mergeWith: MagicComment<Key, Value>): MagicComment<Key, Value>

    /**
     * Converts the magic comment to the lines that the MagicComment would represent in a magic comment code block.
     * Does not include the magic comment prefix and each line has no trailing or leading whitespace.
     */
    fun toCommentString(): List<String> = ArrayList<String>().apply {
        keyValueStore.entries.forEach { (key, values) ->
            if (values.isEmpty()) {
                add(key.toString())
            }

            values.forEach { value ->
                add("$key = $value")
            }
        }
    }

    /**
     * @see [containsKey]
     */
    operator fun contains(key: MagicKey<Key>) = containsKey(key)

    /**
     *  @see [merge]
     */
    operator fun plus(mergeWith: MagicComment<Key, Value>) = merge(mergeWith)

    override fun toString() = keyValueStore.toString()
}

/**
 * See [MagicComment], but mutable.
 *
 * @author Hannah Schellekens
 */
open class MutableMagicComment<Key, Value> : MagicComment<Key, Value>() {

    /**
     * Adds a new key-value pair to the magic comment.
     *
     * If the key already exists, the value will be added to the pool of values for the given key.
     * When the value is `null`, no value is added, but the key is.
     */
    fun addValue(key: MagicKey<Key>, value: Value?) {
        val list = keyValueStore[key.key] ?: ArrayList()
        value?.let { list.add(it) }
        keyValueStore[key.key] = list
    }

    /**
     * Removes all key-value pairs with the given key from the comment.
     *
     * @return The previous value associated with the key, or `null` if the key was not present in the map.
     */
    fun removeKey(key: MagicKey<Key>): List<Value>? = keyValueStore.remove(key.key)

    /**
     * Removes all key-value pairs from the magic comment.
     */
    fun clear() = keyValueStore.clear()

    /**
     * See [merge], but then modifies `this` comment instead of creating a new one.
     */
    private fun mergeModify(mergeWith: MagicComment<Key, Value>) {
        mergeWith.keySet().forEach keyLoop@{ key ->
            val magicKey = CustomMagicKey(key)
            val values = mergeWith.values(magicKey) ?: return@keyLoop

            values.forEach { value ->
                addValue(magicKey, value)
            }

            // There are no values, so just add the key
            if (values.isEmpty()) {
                addValue(magicKey, null)
            }
        }
    }

    override fun merge(mergeWith: MagicComment<Key, Value>): MagicComment<Key, Value> {
        val result = MutableMagicComment<Key, Value>()
        result += this
        result += mergeWith
        return result
    }

    /**
     * @see [mergeModify]
     */
    operator fun plusAssign(mergeWith: MagicComment<Key, Value>) = mergeModify(mergeWith)

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        return other is MutableMagicComment<*, *>
    }

    override fun hashCode(): Int {
        return javaClass.hashCode()
    }
}

package nl.hannahsten.texifyidea.lang.magic

/**
 * @author Hannah Schellekens
 */
interface MagicCommentParser<Key, Value> {

    /**
     * Produces a proper magic comments.
     */
    fun parse(): MagicComment<Key, Value>
}

package nl.hannahsten.texifyidea.lang.magic

/**
 * A list of all scopes on which magic comments can operate.
 *
 * Magic comments will always operate on (or target) the first instance of the given scope
 *
 * @author Hannah Schellekens
 */
enum class MagicCommentScope {

    /**
     * Targets the complete file.
     */
    FILE,

    /**
     * Targets the following environment.
     */
    ENVIRONMENT,

    /**
     * Targets the following math environment (both inline and display).
     */
    MATH_ENVIRONMENT,

    /**
     * Targets the following command.
     */
    COMMAND,

    /**
     * Targets the following content group.
     */
    GROUP;

    companion object {

        /**
         * Contains all comment scopes.
         */
        @JvmStatic
        val ALL_SCOPES = values().toSet()
    }
}

/**
 * A set of scopes that only contains this scope.
 */
fun MagicCommentScope.singleScope() = setOf(this)

@file:JvmName("MagicComments")

package nl.hannahsten.texifyidea.lang.magic

import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * Adds the given magic comment as an actual comment to the file.
 */
fun <Key, Value> PsiFile.addMagicComment(magicComment: MagicComment<Key, Value>) {
    val toInsert = magicComment.toCommentString().joinToString("\n") { "%! $it" }
    document()?.insertString(0, "$toInsert\n")
}

/**
 * Adds the given magic comment as an actual comment to this environment.
 */
fun <Key, Value> LatexEnvironment.addMagicComment(magicComment: MagicComment<Key, Value>) {
    addMagicCommentOnPreviousLine(magicComment)
}

/**
 * Adds the given magic comment as an actual comment to this math environment.
 */
fun <Key, Value> LatexMathEnvironment.addMagicComment(magicComment: MagicComment<Key, Value>) {
    addMagicCommentOnPreviousLine(magicComment)
}

/**
 * Adds the given magic comment as an actual comment to this command.
 */
fun <Key, Value> LatexCommands.addMagicComment(magicComment: MagicComment<Key, Value>) {
    addMagicCommentOnPreviousLine(magicComment)
}

/**
 * Adds the given magic comment as an actual comment to this group.
 */
fun <Key, Value> LatexGroup.addMagicComment(magicComment: MagicComment<Key, Value>) {
    val document = containingFile.document() ?: return
    val lines = magicComment.toCommentString()

    // Put the closing brace on a new line.
    val indentation = document.lineIndentationByOffset(textOffset)
    document.replaceString(textOffset + textLength - 1, textOffset + textLength, "\n$indentation}")

    // When it's a one-liner, put it on the opening brace line and put the closing brace on a new line.
    if (lines.size == 1) {
        val toInsert = " %! ${lines.first()}\n$indentation"
        document.insertString(textOffset + 1, toInsert)
    }
    // Otherwise create a block on the new line.
    else {
        val toInsert = lines.joinToString("\n") { "$indentation%! $it" }
        document.insertString(textOffset + 1, "\n$toInsert\n$indentation")
    }
}

/**
 * Adds the given magic comment on the line preceding this psi element, keeping indentation into account.
 */
private fun <Key, Value> PsiElement.addMagicCommentOnPreviousLine(magicComment: MagicComment<Key, Value>) {
    val document = containingFile.document() ?: return
    val indentation = document.lineIndentationByOffset(textOffset)
    val toInsert = magicComment.toCommentString().joinToString("\n$indentation") { "%! $it" }
    document.insertString(textOffset, "$toInsert\n$indentation")
}

/**
 * Adds a magic comment to the given scope of a certain psielement.
 * Does nothing when there is no valid place to put the magic comment.
 */
fun <Key, Value> PsiElement.addMagicComment(magicComment: MagicComment<Key, Value>, scope: MagicCommentScope) {
    when (scope) {
        MagicCommentScope.FILE -> containingFile?.addMagicComment(magicComment)
        MagicCommentScope.ENVIRONMENT -> parentOfType(LatexEnvironment::class)?.addMagicComment(magicComment)
        MagicCommentScope.MATH_ENVIRONMENT -> parentOfType(LatexMathEnvironment::class)?.addMagicComment(magicComment)
        MagicCommentScope.COMMAND -> parentOfType(LatexCommands::class)?.addMagicComment(magicComment)
        MagicCommentScope.GROUP -> parentOfType(LatexGroup::class)?.addMagicComment(magicComment)
    }
}

/**
 * Adds a magic comment to this psi element based on its subtype.
 */
fun <Key, Value> PsiElement.addMagicCommentToPsiElement(magicComment: MagicComment<Key, Value>) {
    when (this) {
        is PsiFile -> this.addMagicComment(magicComment)
        is LatexEnvironment -> this.addMagicComment(magicComment)
        is LatexMathEnvironment -> this.addMagicComment(magicComment)
        is LatexCommands -> this.addMagicComment(magicComment)
        is LatexGroup -> this.addMagicComment(magicComment)
        else -> error("Unsupported PsiElement type ${this.javaClass}")
    }
}

/**
 * Finds all the consecutive (sibling) magic comments relative to a PsiElement.
 * Ignores regular comments.
 *
 * @param initial
 *          Fetches the first candidate comment.
 * @param next
 *          Fetches the next candidate comment.
 * @param reversed
 *          Whether the magic comments must be inserted in reversed order (`true`) or in regular walking order (`false`).
 * @return The parsed magic comment.
 */
fun PsiElement.magicCommentLookup(
    initial: PsiElement.() -> PsiElement?,
    next: PsiElement.() -> PsiElement?,
    reversed: Boolean = false
): MagicComment<String, String> {
    val commentLines = LinkedList<String>()

    // Scan (backward) through all the magic comments preceding the element.
    var current: PsiElement? = initial() ?: return MagicComment.empty()

    // Stop searching when an element is found that is not a magic comment or that is null
    // (the scan ends at the first element that is not a magic comment).
    while (current.containsMagicComment()) {
        // Only consider magic comments
        val commentText = current?.text ?: continue
        current = current.next()

        // Collect magic comment contents.
        if (reversed) {
            commentLines.addFirst(commentText)
        }
        else commentLines.add(commentText)
    }

    // Parse all collected magic comments.
    val parser = TextBasedMagicCommentParser(commentLines)
    return parser.parse()
}

/**
 * Parses the magic comments that supercede this psi element.
 *
 * @param initial
 *          Fetches the first candidate comment.
 */
fun PsiElement.forwardMagicCommentLookup(initial: PsiElement.() -> PsiElement?) = magicCommentLookup(
    initial,
    PsiElement::nextLeafIgnoreWhitespace,
    reversed = false
)

/**
 * Parses the magic comments that precede this psi element.
 *
 * @param initial
 *          Fetches the first candidate comment.
 */
fun PsiElement.backwardMagicCommentLookup(initial: PsiElement.() -> PsiElement?) = magicCommentLookup(
    initial,
    PsiElement::previousSiblingIgnoreWhitespace,
    reversed = true
)

/**
 * Get the magic comment that directly targets this psi file.
 */
fun PsiFile.magicComment(): MagicComment<String, String> {
    // Find first real element (so the one under LatexContent), which, if it is a magic comment, will be under a LatexNoMathContent
    return forwardMagicCommentLookup { firstChildOfType(LatexNoMathContent::class)?.firstChildIgnoringWhitespaceOrNull() }
}

/**
 * Get the magic comment that directly precedes the environment.
 */
fun LatexEnvironment.magicComment(): MagicComment<String, String> {
    val outerContent = parentOfType(LatexNoMathContent::class) ?: return MagicComment.empty()
    return outerContent.backwardMagicCommentLookup { previousSiblingIgnoreWhitespace() }
}

/**
 * Get the (merged) magic comments that are targeted to this environment, all parent environments and the whole file.
 */
fun LatexEnvironment.allMagicComments(): MagicComment<String, String> {
    val result = MutableMagicComment<String, String>()

    // Direct comments.
    result += magicComment()

    // Parents.
    addParentMagicComments(result)

    return result
}

/**
 * Get the magic comment that directly precedes the math environment.
 */
fun LatexMathEnvironment.magicComment(): MagicComment<String, String> {
    // Because of current parser quirks, there are two scenarios to find the magic comments:
    // 1. When the magic comment is the first element in an environment, they are the previous sibling of the
    //    environment content element.
    // 2. Otherwise they are previous siblings of the direct LatexContent parent of the math environment.

    val parentEnvironmentContent = parentOfType(LatexEnvironmentContent::class)
    val parentEnvironmentContentPreviousSibling = parentEnvironmentContent?.previousSiblingIgnoreWhitespace()

    // Case 1.
    if (parentEnvironmentContentPreviousSibling is PsiComment) {
        return parentEnvironmentContent.backwardMagicCommentLookup { previousSiblingIgnoreWhitespace() }
    }

    // Case 2.
    val outerContent = parentOfType(LatexNoMathContent::class) ?: return MagicComment.empty()
    return outerContent.backwardMagicCommentLookup { previousSiblingIgnoreWhitespace() }
}

/**
 * Get the (merged) magic comments that are targeted to this math environment, all parent environments and the whole file.
 */
fun LatexMathEnvironment.allMagicComments(): MagicComment<String, String> {
    val result = MutableMagicComment<String, String>()

    // Direct comments.
    result += magicComment()

    // Parents.
    addParentMagicComments(result)

    return result
}

/**
 * Get the magic comment that directly targets this command.
 */
fun LatexCommands.magicComment(): MagicComment<String, String> {
    val directParentContent = parentOfType(LatexNoMathContent::class) ?: return MagicComment.empty()
    val parentContentPreviousSibling = directParentContent.previousSiblingIgnoreWhitespace()

    if (parentContentPreviousSibling.containsMagicComment()) {
        return backwardMagicCommentLookup { parentContentPreviousSibling }
    }

    return MagicComment.empty()
}

/**
 * Get the (merged) magic comments that are targeted to this command, and all parent environments and the whole file.
 */
fun LatexCommands.allMagicComments(): MagicComment<String, String> {
    val result = MutableMagicComment<String, String>()

    // Direct comments.
    result += magicComment()

    // / Parents.
    addParentMagicComments(result)

    return result
}

/**
 * Get the magic comment that directly targets this group.
 */
fun LatexGroup.magicComment(): MagicComment<String, String> {
    return forwardMagicCommentLookup { firstChildIgnoringWhitespaceOrNull()?.nextSiblingIgnoreWhitespace() }
}

/**
 * Get the (merged) magic comments that are targeted to this group, all parent goups, commands,
 * environments and the whole file.
 */
fun LatexGroup.allMagicComments(): MagicComment<String, String> {
    val result = MutableMagicComment<String, String>()

    // Direct comments.
    result += magicComment()

    // Parents.
    addParentMagicComments(result)

    return result
}

/**
 * Get the direct magic comment of this psi element, or `null` when magic comments are not supported for this element
 * type.
 */
fun PsiElement.magicComment(): MagicComment<String, String>? = when (this) {
    is PsiFile -> this.magicComment()
    is LatexEnvironment -> this.magicComment()
    is LatexMathEnvironment -> this.magicComment()
    is LatexCommands -> this.magicComment()
    is LatexGroup -> this.magicComment()
    else -> null
}

/**
 * Get the (merged) magic comments that are targeted to all the element's parents (including the whole file).
 */
fun PsiElement.allParentMagicComments(): MagicComment<String, String> = MutableMagicComment<String, String>().apply {
    addParentMagicComments(this)
}

/**
 * Adds all the magic comments of the psi element's parents.
 */
private fun PsiElement.addParentMagicComments(result: MutableMagicComment<String, String>) {
    // Direct comments (instead of allMagicComments) are used to prevent duplicate values.

    // Containing file.
    result += containingFile.magicComment()

    // All parent groups.
    parentsOfType<LatexGroup>().filter { it != this }.forEach { group ->
        result += group.magicComment()
    }

    // All parent commands.
    parentsOfType<LatexCommands>().filter { it != this }.forEach { commands ->
        result += commands.magicComment()
    }

    // All parent environments.
    parentsOfType<LatexEnvironment>().filter { it != this }.forEach { environment ->
        result += environment.magicComment()
    }

    // All parent math environments.
    parentsOfType<LatexMathEnvironment>().filter { it != this }.forEach { mathEnvironment ->
        result += mathEnvironment.magicComment()
    }
}

/**
 * Checks if the magic comment has a key `key` which has at least one occurence of the value `value`.
 */
fun MagicComment<String, String>.containsPair(key: String, value: String): Boolean {
    val magicKey = CustomMagicKey(key)
    val magicValues = values(magicKey) ?: return false
    return value in magicValues
}

package nl.hannahsten.texifyidea.lang.magic

import nl.hannahsten.texifyidea.lang.Described

/**
 * Represents a key from a [MagicComment]s.
 *
 * @author Hannah Schellekens
 */
interface MagicKey<Key> : Described {

    /**
     * The key used in [MagicComment]. Should not contain spaces.
     */
    val key: Key

    /**
     * Textual documentation about this key.
     *
     * The contents of this property contain some human-readable information about the key.
     */
    val documentation: String
        get() = ""

    /**
     * Denotes which scopes the magic key can target, i.e. in which contexts the keys can be used in a useful way.
     */
    val targets: Set<MagicCommentScope>
        get() = MagicCommentScope.ALL_SCOPES

    override val description: String
        get() = documentation
}

package nl.hannahsten.texifyidea.lang.magic

import nl.hannahsten.texifyidea.lang.magic.TextBasedMagicCommentParser.Companion.COMMENT_PREFIX
import java.util.*

/**
 * Converts all provided strings to a single magic comments.
 *
 * Assumes that all provided comments start with the prefix [COMMENT_PREFIX].
 *
 * All keys are stored in lowercase.
 *
 * @author Hannah Schellekens
 */
open class TextBasedMagicCommentParser(private val comments: List<String>) : MagicCommentParser<String, String> {

    companion object {

        /**
         * The prefix that a magic comment has including the original comment %.
         *
         * This regex also matches whitespace after the tag.
         */
        @JvmStatic
        val COMMENT_PREFIX = Regex("""^(%\s*!\s*(TeX)?\s*)""", RegexOption.IGNORE_CASE)

        /**
         * Matches against a key assignment.
         */
        private val KEY_ASSIGNMENT = Regex("""^([a-zA-Z\s]+)\s*=""")
    }

    override fun parse(): MagicComment<String, String> = MutableMagicComment<String, String>().apply {
        // The key that is currently being processed, or null when there is no key yet.
        var key: String? = null

        // Collects the value of a key-value pair.
        var contentBuffer = StringBuilder()

        /** Adds the current key with converts to the magic comment. Also resets the key and content. */
        fun pushKeyValuePair() {
            if (key == null) return
            addValue(key!!.trim().asKey(), contentBuffer.toString().trim())
            contentBuffer = StringBuilder()
            key = null
        }

        comments.forEach { comment ->
            val line = comment.trimStart().replace(COMMENT_PREFIX, "").trimEnd()

            // Finish the key-value pair after an empty line is found.
            if (line.isEmpty()) {
                pushKeyValuePair()
                return@forEach
            }

            // Tries to find the beginning of a key assignment in the form of 'Key ='
            val keyMatcher = KEY_ASSIGNMENT.toPattern().matcher(line)

            // A key has been found, so a new key is created.
            when {
                keyMatcher.find() -> {
                    // Register previous key/value pair.
                    if (key != null) {
                        pushKeyValuePair()
                    }

                    key = keyMatcher.group(1).lowercase(Locale.getDefault())

                    val parts = line.split("=")
                    val contents = parts.subList(1, parts.size).joinToString("=")
                    contentBuffer.append(contents).append(' ')
                }
                // There is no key assignment.
                // Check if there is no previous key defined, because if there isn't, it will register an value-less key.
                key == null -> {
                    addValue(
                        key = line.split(" ").first().asKey(),
                        value = line.trim().split(" ").drop(1).joinToString(" ").let {
                            it.ifEmpty { null }
                        }
                    )
                }
                // Fill up contents of the existing key.
                // Each newline is considered a space.
                else -> {
                    contentBuffer.append(line.trim()).append(' ')
                }
            }
        }

        pushKeyValuePair()
    }

    private fun String.asKey() = DefaultMagicKeys.values().find { it.key == this } ?: CustomMagicKey(this)
}

/**
 * Creates a new [TextBasedMagicCommentParser] from the list of magic comments.
 */
fun List<String>.textBasedMagicCommentParser() = TextBasedMagicCommentParser(this)

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.psi.PsiReference
import nl.hannahsten.texifyidea.psi.BibtexDefinedString
import nl.hannahsten.texifyidea.reference.BibtexStringReference

abstract class BibtexDefinedStringImplMixin(node: ASTNode) : BibtexDefinedString, ASTWrapperPsiElement(node) {
    /**
     * Get a reference to the declaration of the string variable.
     */
    override fun getReference(): PsiReference {
        return BibtexStringReference(this)
    }
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.StubBasedPsiElementBase
import com.intellij.lang.ASTNode
import com.intellij.openapi.paths.WebReference
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.psi.stubs.IStubElementType
import com.intellij.psi.tree.IElementType
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.index.stub.BibtexEntryStub
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.getIdentifier
import nl.hannahsten.texifyidea.util.parser.getTagContent
import org.jetbrains.annotations.NonNls

abstract class BibtexEntryImplMixin : BibtexEntry, StubBasedPsiElementBase<BibtexEntryStub> {

    constructor(stub: BibtexEntryStub, nodeType: IStubElementType<*, *>) : super(stub, nodeType)
    constructor(node: ASTNode) : super(node)
    constructor(stub: BibtexEntryStub?, nodeType: IElementType?, node: ASTNode?) : super(stub, nodeType, node)

    /**
     * Get all the references from a BibTeX entry.
     */
    override fun getReferences(): Array<PsiReference> {
        val urls = CommandMagic.bibUrls
            .map { this.getTagContent(it) }
            .filter { it.isNotBlank() }

        if (urls.isNotEmpty()) {
            // We cannot simply return urls.map { WebReference(this, it) } because
            // then IJ cannot find the range of the url in the entry (and thus it
            // doesn't know which text to underline). So we have to return a list of
            // WebReference(this, text range) instead.
            val contentText = this.entryContent?.text ?: return emptyArray()
            val rangesInParent = urls.mapNotNull {
                if (it !in contentText) {
                    null
                }
                else {
                    TextRange.from(contentText.indexOf(it), it.length)
                }
            }

            return rangesInParent.map {
                WebReference(this, it.shiftRight(this.entryContent!!.textOffset - this.textOffset))
            }.toArray(emptyArray())
        }

        return emptyArray()
    }

    override fun setName(name: @NonNls String): PsiElement {
        return this
    }

    override fun getName(): String? {
        val stub = this.stub
        return if (stub != null) stub.name else this.getIdentifier()
    }

    override fun getNameIdentifier(): PsiElement {
        return this
    }

    override fun toString(): String {
        return this.text
    }
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.search.GlobalSearchScope
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexId
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.remove

abstract class BibtexIdImplMixin(node: ASTNode) : BibtexId, ASTWrapperPsiElement(node) {

    override fun getNameIdentifier(): PsiElement {
        return this
    }

    override fun setName(name: String): PsiElement {
        // Replace the complete bibtex entry to automatically update the index (which is on entries, not ids)
        val entry = this.firstParentOfType(BibtexEntry::class)
        val oldName = this.name
        val newText = entry?.text?.replaceFirst(oldName, name) ?: return this
        val newElement = PsiFileFactory.getInstance(this.project).createFileFromText("DUMMY.tex", BibtexLanguage, newText, false, true).firstChild
        entry.parent.node.replaceChild(entry.node, newElement.node)
        return this
    }

    override fun getName(): String {
        return this.text
    }

    override fun delete() {
        val text = this.text ?: return

        val searchScope = GlobalSearchScope.fileScope(this.containingFile)
        BibtexEntryIndex().getEntryByName(text, this.project, searchScope).forEach {
            it.remove()
        }
    }
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiReference
import nl.hannahsten.texifyidea.psi.BibtexContent
import nl.hannahsten.texifyidea.psi.BibtexKey
import nl.hannahsten.texifyidea.psi.BibtexTag
import nl.hannahsten.texifyidea.reference.SimpleFileReference
import nl.hannahsten.texifyidea.util.magic.FileMagic
import nl.hannahsten.texifyidea.util.parser.firstChildOfType

abstract class BibtexTagImplMixin(node: ASTNode) : BibtexTag, ASTWrapperPsiElement(node) {

    override fun getReferences(): Array<PsiReference> {
        val key = this.firstChildOfType(BibtexKey::class)?.text ?: return emptyArray()
        if (key !in FileMagic.bibtexFileKeys) return emptyArray()
        val content = this.firstChildOfType(BibtexContent::class)?.text ?: return emptyArray()

        // Mendeley contents are of the form {:full/path/to/file1.pdf:pdf;:file2.pdf:pdf}
        val contentParts = content.trimStart('{').trimEnd('}').split(";")
        val references = mutableListOf<PsiReference>()

        for (part in contentParts) {
            if (SystemInfo.isWindows) {
                references.add(SimpleFileReference(this, part))
                // The Mendeley format on Windows seems too inconsistent to work with, especially since the : has multiple meanings
            }
            else {
                // Possible split on : (maybe not necessary)
                val partSplit = part.split(":")
                if (partSplit.size > 1) {
                    // Assume we are in Mendeley format
                    val filePath = "/" + partSplit[1]
                    val textRange = TextRange.from(this.text.indexOf(partSplit[1]), filePath.length)
                    references.add(SimpleFileReference(this, filePath, textRange))
                }
                else {
                    // Assume we already have the filepath
                    val filePath = partSplit[0]
                    references.add(SimpleFileReference(this, filePath))
                }
            }
        }

        return references.toTypedArray()
    }
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.util.parser.getOptionalParameterMapFromParameters

abstract class LatexBeginCommandImplMixin(node: ASTNode) : LatexBeginCommand, ASTWrapperPsiElement(node) {

    override fun getOptionalParameterMap() = getOptionalParameterMapFromParameters(this.parameterList)

    override fun getRequiredParameters() = nl.hannahsten.texifyidea.util.parser.getRequiredParameters(this.parameterList)
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.StubBasedPsiElementBase
import com.intellij.lang.ASTNode
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.PsiReference
import com.intellij.psi.stubs.IStubElementType
import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.index.stub.LatexCommandsStub
import nl.hannahsten.texifyidea.psi.LatexCommandWithParams
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.psi.LatexVisitor
import nl.hannahsten.texifyidea.reference.CommandDefinitionReference
import nl.hannahsten.texifyidea.util.labels.getLabelReferenceCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.*

/**
 * This class is a mixin for LatexCommandsImpl.
 */
abstract class LatexCommandsImplMixin : StubBasedPsiElementBase<LatexCommandsStub?>, PsiNameIdentifierOwner, LatexCommands, LatexCommandWithParams {

    @JvmField
    var name: String? = null

    constructor(stub: LatexCommandsStub, nodeType: IStubElementType<*, *>) : super(stub, nodeType)
    constructor(node: ASTNode) : super(node)
    constructor(stub: LatexCommandsStub?, nodeType: IElementType?, node: ASTNode?) : super(stub, nodeType, node)

    override fun toString(): String {
        return "LatexCommandsImpl(COMMANDS)[STUB]{" + getName() + "}"
    }

    override fun getTextOffset(): Int {
        val name = getName()
        return if (name == null) {
            super.getTextOffset()
        }
        else {
            val offset = node.text.indexOf(name)
            if (offset == -1) super.getTextOffset() else node.startOffset + offset
        }
    }

    override fun accept(visitor: PsiElementVisitor) {
        if (visitor is LatexVisitor) {
            accept(visitor)
        }
        else {
            super.accept(visitor)
        }
    }

    override fun getNameIdentifier(): PsiElement? {
        return this
    }

    override fun getName(): String? {
        val stub = this.stub
        return if (stub != null) stub.name else this.commandToken.text
    }

    override fun setName(newName: String): PsiElement {
        var newText = this.text.replace(getName() ?: return this, newName)
        if (!newText.startsWith("\\"))
            newText = "\\" + newText
        val newElement = LatexPsiHelper(this.project).createFromText(newText).firstChild
        val oldNode = this.node
        val newNode = newElement.node
        this.parent?.node?.replaceChild(oldNode, newNode)
        return this
    }

    /**
     * References which do not need a find usages to work on lower level psi elements (normal text) can be implemented on the command, otherwise they are in {@link LatexPsiImplUtil#getReference(LatexParameterText)}.
     * For more info and an example, see {@link nl.hannahsten.texifyidea.reference.LatexLabelParameterReference}.
     *
     * Do not use this if you just want references of a particular type, to avoid resolving references (which can be expensive) that are not needed.
     * For example, use LatexCommands.getFileArgumentsReferences() for InputFileReferences.
     */
    override fun getReferences(): Array<PsiReference> {
        val requiredParameters = getRequiredParameters(this)
        val firstParam = requiredParameters.getOrNull(0)

        val references = mutableListOf<PsiReference>()

        // If it is a reference to a label (used for autocompletion, do not confuse with reference resolving from LatexParameterText)
        if (this.project.getLabelReferenceCommands().contains(this.commandToken.text) && firstParam != null) {
            references.addAll(extractLabelReferences(this, requiredParameters))
        }

        // If it is a reference to a file
        references.addAll(this.getFileArgumentsReferences())

        if (CommandMagic.urls.contains(this.getName()) && firstParam != null) {
            references.addAll(this.extractUrlReferences(firstParam))
        }

        // Else, we assume the command itself is important instead of its parameters,
        // and the user is interested in the location of the command definition
        val definitionReference = CommandDefinitionReference(this)
        // Only create a reference if there is something to resolve to, otherwise autocompletion won't work
        if (definitionReference.multiResolve(false).isNotEmpty()) {
            references.add(definitionReference)
        }

        return references.toTypedArray()
    }

    /**
     * Get the reference for this command, assuming it has exactly one reference (return null otherwise).
     */
    override fun getReference(): PsiReference? {
        return this.references.firstOrNull()
    }

    override fun getOptionalParameterMap() = getOptionalParameterMapFromParameters(this.parameterList)

    override fun getRequiredParameters() = getRequiredParameters(this.parameterList)
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.StubBasedPsiElementBase
import com.intellij.lang.ASTNode
import com.intellij.psi.ElementManipulators
import com.intellij.psi.LiteralTextEscaper
import com.intellij.psi.PsiLanguageInjectionHost
import com.intellij.psi.stubs.IStubElementType
import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.index.stub.LatexEnvironmentStub
import nl.hannahsten.texifyidea.psi.LatexEnvironment

abstract class LatexEnvironmentImplMixin : LatexEnvironment, StubBasedPsiElementBase<LatexEnvironmentStub> {

    constructor(stub: LatexEnvironmentStub, nodeType: IStubElementType<*, *>) : super(stub, nodeType)
    constructor(node: ASTNode) : super(node)
    constructor(stub: LatexEnvironmentStub?, nodeType: IElementType?, node: ASTNode?) : super(stub, nodeType, node)

    override fun isValidHost(): Boolean {
        return true
    }

    override fun updateText(text: String): PsiLanguageInjectionHost {
        return ElementManipulators.handleContentChange(this, text)
    }

    override fun createLiteralTextEscaper(): LiteralTextEscaper<LatexEnvironment> {
        return LiteralTextEscaper.createSimple(this)
    }
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.LatexGroup
import nl.hannahsten.texifyidea.psi.LatexOptionalParamContent

abstract class LatexKeyValKeyImplMixin(node: ASTNode) : ASTWrapperPsiElement(node) {

    override fun toString(): String {
        // This is ugly, but element.children returns only composite children and other querying methods are recursive
        val result = ArrayList<PsiElement>()
        var psiChild = this.firstChild
        while (psiChild != null) {
            result.add(psiChild)
            psiChild = psiChild.nextSibling
        }
        return result.joinToString(separator = "") {
            when (it) {
                is LatexOptionalParamContent -> it.group?.let { g -> g.content?.text ?: "" } ?: it.text
                is LatexGroup -> it.content?.text ?: ""
                else -> it.text
            }
        }
    }
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import nl.hannahsten.texifyidea.psi.LatexKeyValValue

abstract class LatexKeyValValueImplMixin(node: ASTNode) : LatexKeyValValue, ASTWrapperPsiElement(node) {

    override fun toString(): String {
        return keyValContentList.joinToString(separator = "") {
            when {
                it.parameterText != null -> it.parameterText!!.text
                it.parameterGroup != null -> it.parameterGroup!!.parameterGroupText!!.text
                else -> ""
            }
        }
    }
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.psi.ElementManipulators
import com.intellij.psi.LiteralTextEscaper
import com.intellij.psi.PsiLanguageInjectionHost
import nl.hannahsten.texifyidea.psi.LatexParameter

abstract class LatexParameterImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), LatexParameter {

    override fun isValidHost(): Boolean {
        return true
    }

    override fun updateText(text: String): PsiLanguageInjectionHost {
        return ElementManipulators.handleContentChange(this, text)
    }

    override fun createLiteralTextEscaper(): LiteralTextEscaper<LatexParameter> {
        return LiteralTextEscaper.createSimple(this)
    }
}

package nl.hannahsten.texifyidea.psi.impl

import com.intellij.extapi.psi.ASTWrapperPsiElement
import com.intellij.lang.ASTNode
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import nl.hannahsten.texifyidea.lang.commands.LatexGlossariesCommand
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.reference.BibtexIdReference
import nl.hannahsten.texifyidea.reference.LatexEnvironmentReference
import nl.hannahsten.texifyidea.reference.LatexGlossaryReference
import nl.hannahsten.texifyidea.reference.LatexLabelParameterReference
import nl.hannahsten.texifyidea.util.isFigureLabel
import nl.hannahsten.texifyidea.util.labels.extractLabelName
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands
import nl.hannahsten.texifyidea.util.labels.getLabelReferenceCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.remove

abstract class LatexParameterTextImplMixin(node: ASTNode) : LatexParameterText, ASTWrapperPsiElement(node) {

    /**
     * If the normal text is the parameter of a \ref-like command, get the references to the label declaration.
     */
    override fun getReferences(): Array<PsiReference> {
        // If the command is a label reference
        // NOTE When adding options here, also update getNameIdentifier below
        return when {
            this.project.getLabelReferenceCommands().contains(this.firstParentOfType(LatexCommands::class)?.name) -> {
                arrayOf(LatexLabelParameterReference(this))
            }
            // If the command is a bibliography reference
            CommandMagic.bibliographyReference.contains(this.firstParentOfType(LatexCommands::class)?.name) -> {
                arrayOf(BibtexIdReference(this))
            }
            // If the command is an \end command (references to \begin)
            this.firstParentOfType(LatexEndCommand::class) != null -> {
                arrayOf(LatexEnvironmentReference(this))
            }
            // If the command is a glossary reference
            CommandMagic.glossaryReference.contains(this.firstParentOfType(LatexCommands::class)?.name) -> {
                arrayOf(LatexGlossaryReference(this))
            }

            else -> {
                emptyArray<PsiReference>()
            }
        }
    }

    /**
     * If [getReferences] returns one reference return that one, null otherwise.
     */
    override fun getReference(): PsiReference? {
        return references.firstOrNull()
    }

    override fun getNameIdentifier(): PsiElement? {
        // Because we do not want to trigger the NonAsciiCharactersInspection when the LatexParameterText is not an identifier
        // (think non-ASCII characters in a \section command), we return null here when the this is not an identifier
        // It is important not to return null for any identifier, otherwise exceptions like "Throwable: null byMemberInplaceRenamer" may occur
        val name = this.firstParentOfType(LatexCommands::class)?.name
        val environmentName = this.firstParentOfType(LatexEnvironment::class)?.getEnvironmentName()
        if (!CommandMagic.labelReferenceWithoutCustomCommands.contains(name) &&
            !CommandMagic.labelDefinitionsWithoutCustomCommands.contains(name) &&
            !CommandMagic.bibliographyReference.contains(name) &&
            !CommandMagic.labelAsParameter.contains(name) &&
            !CommandMagic.glossaryEntry.contains(name) &&
            !EnvironmentMagic.labelAsParameter.contains(environmentName) &&
            this.firstParentOfType(LatexEndCommand::class) == null &&
            this.firstParentOfType(LatexBeginCommand::class) == null
        ) {
            return null
        }
        return this
    }

    override fun setName(name: String): PsiElement {
        /**
         * Build a new PSI this where [old] is replaced with [new] and replace the old PSI this
         */
        fun replaceInCommand(command: LatexCommands?, old: String, new: String) {
            // This could go wrong in so many cases
            val labelText = command?.text?.replaceFirst(old, new) ?: "${command?.name}{$new}"
            val newElement = LatexPsiHelper(this.project).createFromText(labelText).firstChild
            val oldNode = command?.node
            val newNode = newElement.node
            if (oldNode == null) {
                command?.parent?.node?.addChild(newNode)
            }
            else {
                command.parent.node.replaceChild(oldNode, newNode)
            }
        }

        val command = this.firstParentOfType(LatexCommands::class)
        val environment = this.firstParentOfType(LatexEnvironment::class)
        // If we want to rename a label
        if (CommandMagic.reference.contains(command?.name) || this.project.getLabelDefinitionCommands()
            .contains(command?.name)
        ) {
            // Get a new psi this for the complete label command (\label included),
            // because if we replace the complete command instead of just the normal text
            // then the indices will be updated, which is necessary for the reference resolve to work
            val oldLabel = this.extractLabelName()
            replaceInCommand(command, oldLabel, name)
        }
        else if (CommandMagic.labelAsParameter.contains(command?.name) || EnvironmentMagic.labelAsParameter.contains(
                environment?.getEnvironmentName()
            )
        ) {
            val helper = LatexPsiHelper(this.project)

            // If the label name is inside a group, keep the group
            val value = if (this.parentOfType(LatexParameterGroupText::class) != null) {
                "{$name}"
            }
            else {
                name
            }
            helper.setOptionalParameter(command ?: environment!!.beginCommand, "label", value)
        }
        else if (CommandMagic.glossaryReference.contains(command?.name) || CommandMagic.glossaryEntry.contains(command?.name)) {
            // This assumes that glossary entry commands (e.g. \newglossaryentry) as well as glossary references (e.g. \gls)
            // have the glossary label as their first required parameter. This is true for all currently supported glossary
            // commands, but might change in the future.
            if (command != null) {
                val glossaryLabel = LatexGlossariesCommand.extractGlossaryLabel(command) ?: ""
                replaceInCommand(command, glossaryLabel, name)
            }
        }
        else if (this.firstParentOfType(LatexEndCommand::class) != null || this.firstParentOfType(LatexBeginCommand::class) != null) {
            // We are renaming an environment, text in \begin or \end
            val newElement = LatexPsiHelper(this.project).createFromText(name).firstChild
            val oldNode = this.node
            val newNode = newElement.node
            this.parent.node.replaceChild(oldNode, newNode)
        }
        // Else, this is not renamable

        return this
    }

    override fun getName(): String {
        return this.text ?: ""
    }

    override fun delete() {
        val cmd = this.parentOfType(LatexCommands::class) ?: return
        if (cmd.isFigureLabel()) {
            // Look for the NoMathContent that is around the environment, because that is the PsiElement that has the
            // whitespace and other normal text as siblings.
            cmd.parentOfType(LatexEnvironment::class)
                ?.parentOfType(LatexNoMathContent::class)
                ?.remove()
        }
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinecommand

import com.intellij.java.refactoring.JavaRefactoringBundle
import com.intellij.psi.PsiElement
import com.intellij.usageView.UsageViewBundle
import com.intellij.usageView.UsageViewDescriptor
import nl.hannahsten.texifyidea.file.LatexFile

/**
 * Required file for the IDEA refactoring workflow. Parts have been borrowed from the java inline descriptor
 *
 * @see com.intellij.refactoring.inline.InlineViewDescriptor
 *
 * @author jojo2357
 */
class LatexInlineCommandDescriptor(private val myElement: PsiElement) : UsageViewDescriptor {

    override fun getElements(): Array<PsiElement> {
        return arrayOf(myElement)
    }

    override fun getProcessedElementsHeader(): String {
        return if (this.myElement is LatexFile)
            "Command to inline"
        else
            "Unknown element"
    }

    override fun getCodeReferencesText(usagesCount: Int, filesCount: Int): String {
        return JavaRefactoringBundle.message(
            "invocations.to.be.inlined",
            UsageViewBundle.getReferencesString(usagesCount, filesCount)
        )
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinecommand

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.definitionCommand

/**
 * Creates an inline file dialog to select what to refactor. Parts have been borrowed from the java inline method dialog
 *
 * @see com.intellij.refactoring.inline.InlineMethodDialog
 *
 * @author jojo2357
 */
class LatexInlineCommandDialog(
    project: Project?,
    private val myDefinition: LatexCommands,
    private val myReference: PsiElement?,
    invokedOnReference: Boolean,
) :
    LatexInlineDialog(project, myDefinition, invokedOnReference) {

    init {
        title = "Inline Command"
        init()
    }

    override fun getNameLabelText(): String {
        return if (getNumberOfOccurrences() > -1) "Command " + myDefinition.name + " has " + getNumberOfOccurrences() + " ocurrences"
        else "Command " + myDefinition.name
    }

    override fun getInlineThisText(): String {
        return "Inline this and keep the command"
    }

    override fun getInlineAllText(): String {
        return if (myDefinition.isWritable) "Inline all and remove the command" else "All invocations in project"
    }

    override fun getKeepTheDeclarationText(): String {
        return if (myDefinition.isWritable) "Inline all and keep the command" else super.getKeepTheDeclarationText()
    }

    override fun doAction() {
        invokeRefactoring(
            LatexInlineCommandProcessor(
                project,
                myDefinition,
                myReference,
                isInlineThisOnly,
                isKeepTheDeclaration,
                GlobalSearchScope.projectScope(myProject)
            )
        )
        updateSettingsPreferences()
    }

    override fun getNumberOfOccurrences(): Int {
        return if (myDefinition.definitionCommand() == null) 0 else super.getNumberOfOccurrences(myDefinition.definitionCommand())
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinecommand

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase.getElementAtCaret
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.Log
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import nl.hannahsten.texifyidea.util.getCommandsInFiles
import nl.hannahsten.texifyidea.util.parser.*

/**
 * Allows for inlining an include command. Does not do the inlining itself.
 *
 * @author jojo2357
 */
class LatexInlineCommandHandler : LatexInlineHandler() {

    override fun canInlineElement(element: PsiElement?): Boolean {
        return canInlineLatexElement(
            element
        )
    }

    override fun inlineElement(project: Project, editor: Editor?, element: PsiElement) {
        // Resolve the file to be inlined
        val inlineCommand: LatexCommands = resolveInlineCommandDefinition(element) ?: return
        val myReference = getReference(editor)

        val dialog = LatexInlineCommandDialog(
            project,
            inlineCommand,
            myReference,
            editor != null && inlineCommand.firstChildOfType(LatexCommands::class)?.textOffset != myReference?.textOffset
        )

        showDialog(dialog, inlineCommand.name ?: "", project)
    }

    companion object {

        /**
         * This is static only so that the unit tests can use it, and also since it can be static
         */
        fun canInlineLatexElement(element: PsiElement?): Boolean {
            if (element !is LatexCommands) {
                return false
            }

            // we will not be dealing with commands that have a map of options
            if (element.getOptionalParameterMap().any { it.value != null })
                return false

            val file = element.containingFile
            val files: MutableSet<PsiFile> = HashSet(file.referencedFileSet())

            val cmds = getCommandsInFiles(files, file)

            val candiateUserCommands = cmds.filter { it.isDefinition() && !it.isEnvironmentDefinition() }

            val canInline = candiateUserCommands.any {
                it.definitionCommand()?.name == element.name
            }

            return canInline
        }

        fun resolveInlineCommandDefinition(element: PsiElement): LatexCommands? {
            val file = element.containingFile
            val files: MutableSet<PsiFile> = HashSet(file.referencedFileSet())

            val cmds = getCommandsInFiles(files, file)

            val candiateUserCommands = cmds.filter { it.isDefinition() && !it.isEnvironmentDefinition() }

            val thisCommandDefinitions = candiateUserCommands.filter {
                it.definitionCommand()?.name == (element as LatexCommands).name
            }

            // There may be an issue where it picks the wrong definition when there are multiple redefinitions
            return thisCommandDefinitions.firstOrNull()
        }

        /**
         * Resolves the command that the inlining was called on. If called from file view then expected return is null
         */
        fun getReference(
            editor: Editor?
        ): PsiElement? {
            if (editor == null) return null
            val inliner = getElementAtCaret(editor)
            return if (inliner is LatexCommands)
                inliner
            else {
                val ref: LatexCommands? = inliner?.firstParentOfType(LatexCommands::class)
                if (ref != null)
                    ref
                else {
                    Log.warn("Could not find the command element for " + (inliner?.text ?: ""))
                    null
                }
            }
        }
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinecommand

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.SearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.refactoring.BaseRefactoringProcessor
import com.intellij.usageView.UsageInfo
import com.intellij.usageView.UsageViewDescriptor
import nl.hannahsten.texifyidea.psi.LatexCommandWithParams
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.parser.definitionCommand

/**
 * Recieves settings and a target and performs the requested refactoring
 *
 * @see com.intellij.refactoring.inline.InlineMethodProcessor
 *
 * @author jojo2357
 */
class LatexInlineCommandProcessor(
    myProject: Project,
    private val inlineCommand: LatexCommands,
    private val originalReference: PsiElement?,
    private val isInlineThisOnly: Boolean,
    private val isKeepTheDeclaration: Boolean,
    private val myScope: SearchScope = GlobalSearchScope.projectScope(myProject)
) : BaseRefactoringProcessor(myProject) {

    override fun createUsageViewDescriptor(usages: Array<out UsageInfo>): UsageViewDescriptor {
        return LatexInlineCommandDescriptor(inlineCommand)
    }

    override fun findUsages(): Array<UsageInfo> {
        val tempreferences =
            ReferencesSearch.search(inlineCommand.definitionCommand()!!, myScope).findAll().asSequence()

        return tempreferences
            .distinct()
            .map(PsiReference::getElement)
            .map(::UsageInfo)
            .toList()
            .toTypedArray()
    }

    override fun performRefactoring(usages: Array<out UsageInfo>) {
        if (isInlineThisOnly) {
            if (originalReference != null)
                ApplicationManager.getApplication().runWriteAction {
                    CommandProcessor.getInstance().executeCommand(myProject, {
                        replaceUsage(originalReference)
                    }, commandName, "Texify")
                }
            else if (!ApplicationManager.getApplication().isUnitTestMode)
                throw IllegalStateException("Inline this requested with no original reference (" + usages.size + ")")
            // else we are probably trying to elicit no response because this is illegal
        }
        else if (usages.isNotEmpty()) {
            ApplicationManager.getApplication().runWriteAction {
                CommandProcessor.getInstance().executeCommand(myProject, {
                    var allSuccessfull = true
                    for (replaceUsage in usages) {
                        val replacereference = replaceUsage.element ?: continue
                        allSuccessfull = allSuccessfull && replaceUsage(replacereference)
                    }
                    if (!isKeepTheDeclaration && allSuccessfull)
                        inlineCommand.delete()
                }, commandName, "Texify")
            }
        }
    }

    /**
     * Does the lifting by...replacing usages with the entirety of the command to be inlined
     *
     * @param psiElement The element to remove and replace with the contents of the command
     */
    private fun replaceUsage(psiElement: PsiElement): Boolean {
        val calledRequiredArgs = (psiElement as? LatexCommandWithParams)?.getRequiredParameters() ?: listOf()
        val calledOptionalArgs = (psiElement as? LatexCommandWithParams)?.getOptionalParameterMap()?.keys?.toList()?.map { it.text } ?: listOf<String>()

        val parentOptionalArgs = (inlineCommand as? LatexCommandWithParams)?.getOptionalParameterMap()?.keys?.toList()?.map { it.text } ?: listOf<String>()

        if (calledOptionalArgs.size > 1)
            return false // error case

        if (parentOptionalArgs.size > 2)
            return false // error case

        val defaultParam = if (parentOptionalArgs.size == 2) parentOptionalArgs[1] else null
        val offsetIndex = if (defaultParam == null) 1 else 2
        val numArgs = if (parentOptionalArgs.isNotEmpty()) Integer.parseInt(parentOptionalArgs[0]) else 0

        val commandParameterRegex = "(?<!\\\\)#(\\d)".toRegex()

        // Assume that the command should be replaced by the contents of the last required parameter
        val functionString = (inlineCommand as LatexCommandWithParams).parameterList.lastOrNull()?.requiredParam?.requiredParamContentList?.joinToString { it.text } ?: return false
        val parameterUsages = commandParameterRegex.findAll(functionString).toSet()
        val actualNumberOfParameters = parameterUsages.maxOfOrNull { Integer.parseInt(it.value.substring(1)) }

        if (actualNumberOfParameters != null && actualNumberOfParameters > numArgs)
            return false

        var outText = functionString

        for (i in calledRequiredArgs.indices) {
            outText = outText.replace("#${i + offsetIndex}", calledRequiredArgs[i])
        }

        if (defaultParam != null) {
            outText = if (calledOptionalArgs.size == 1)
                outText.replace("#1", calledOptionalArgs[0])
            else
                outText.replace("#1", defaultParam)
        }

        val tempFile = LatexPsiHelper(psiElement.project).createFromText(outText)
        psiElement.replace(tempFile.firstChild)

        return true
    }

    override fun getCommandName(): String {
        return "Inlining Command " + this.inlineCommand.name
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinecommand

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.refactoring.JavaRefactoringSettings
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.inline.InlineOptionsDialog

abstract class LatexInlineDialog(project: Project?, genericDefinition: PsiElement, invokedOnReference: Boolean) : InlineOptionsDialog(project, true, genericDefinition) {

    init {
        super.myInvokedOnReference = invokedOnReference
    }

    public abstract override fun doAction()

    abstract fun getNumberOfOccurrences(): Int

    override fun getNumberOfOccurrences(nameIdentifierOwner: PsiNameIdentifierOwner?): Int {
        val tempreferences = ReferencesSearch.search(nameIdentifierOwner as PsiElement).findAll().asSequence()

        return tempreferences
            .distinct()
            .toList().size
    }

    override fun getBorderTitle(): String {
        return RefactoringBundle.message("inline.method.border.title")
    }

    override fun isInlineThis(): Boolean {
        return JavaRefactoringSettings.getInstance().INLINE_METHOD_THIS
    }

    override fun isKeepTheDeclarationByDefault(): Boolean {
        return JavaRefactoringSettings.getInstance().INLINE_METHOD_KEEP
    }

    override fun hasHelpAction(): Boolean {
        return false
    }

    override fun allowInlineAll(): Boolean {
        return true
    }

    protected fun updateSettingsPreferences() {
        val settings = JavaRefactoringSettings.getInstance()
        if (myRbInlineThisOnly.isEnabled && myRbInlineAll.isEnabled) {
            settings.INLINE_METHOD_THIS = isInlineThisOnly
        }
        if (myKeepTheDeclaration != null && myKeepTheDeclaration!!.isEnabled) {
            settings.INLINE_METHOD_KEEP = isKeepTheDeclaration
        }
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinecommand

import com.intellij.lang.Language
import com.intellij.lang.refactoring.InlineActionHandler
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import nl.hannahsten.texifyidea.grammar.LatexLanguage

abstract class LatexInlineHandler : InlineActionHandler() {

    protected fun showDialog(
        dialog: LatexInlineDialog,
        inlineElementName: String,
        project: Project
    ) {
        if (dialog.getNumberOfOccurrences() > 0) {
            if (ApplicationManager.getApplication().isUnitTestMode) {
                try {
                    dialog.doAction()
                }
                finally {
                    dialog.close(DialogWrapper.OK_EXIT_CODE, true)
                }
            }
            else {
                dialog.show()
            }
        }
        else {
            Notification(
                "LaTeX",
                "No usages found",
                "Could not find any usages for $inlineElementName",
                NotificationType.ERROR
            ).notify(project)
        }
    }

    override fun isEnabledForLanguage(language: Language?): Boolean {
        return language == LatexLanguage
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinefile

import com.intellij.java.refactoring.JavaRefactoringBundle
import com.intellij.psi.PsiElement
import com.intellij.usageView.UsageViewBundle
import com.intellij.usageView.UsageViewDescriptor
import nl.hannahsten.texifyidea.file.LatexFile

/**
 * Required file for the IDEA refactoring workflow. Parts have been borrowed from the java inline descriptor
 *
 * @see com.intellij.refactoring.inline.InlineViewDescriptor
 *
 * @author jojo2357
 */
class LatexInlineFileDescriptor(private val myElement: PsiElement) : UsageViewDescriptor {

    override fun getElements(): Array<PsiElement> {
        return arrayOf(myElement)
    }

    override fun getProcessedElementsHeader(): String {
        return if (this.myElement is LatexFile)
            "File to inline"
        else
            "Unknown element"
    }

    override fun getCodeReferencesText(usagesCount: Int, filesCount: Int): String {
        return JavaRefactoringBundle.message(
            "invocations.to.be.inlined",
            UsageViewBundle.getReferencesString(usagesCount, filesCount)
        )
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinefile

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts.DialogTitle
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.refactoring.inlinecommand.LatexInlineDialog

/**
 * Creates an inline file dialog to select what to refactor. Parts have been borrowed from the java inline method dialog
 *
 * @see com.intellij.refactoring.inline.InlineMethodDialog
 *
 * @author jojo2357
 */
class LatexInlineFileDialog(
    project: Project?,
    private val myFile: LatexFile,
    private val myReference: PsiElement?,
    invokedOnReference: Boolean,
) :
    LatexInlineDialog(project, myFile, invokedOnReference) {

    init {
        title = refactoringName
        init()
    }

    override fun getNameLabelText(): String {
        return if (getNumberOfOccurrences() > -1) "File " + myFile.name + " has " + getNumberOfOccurrences() + " ocurrences"
        else "File " + myFile.name
    }

    override fun getInlineThisText(): String {
        return "Inline this and keep the file"
    }

    override fun getInlineAllText(): String {
        return if (myFile.isWritable) "Inline all and remove the file" else "All invocations in project"
    }

    override fun getKeepTheDeclarationText(): String {
        return if (myFile.isWritable) "Inline all and keep the file" else super.getKeepTheDeclarationText()
    }

    override fun doAction() {
        invokeRefactoring(
            LatexInlineFileProcessor(
                project,
                myFile,
                myReference,
                isInlineThisOnly,
                isKeepTheDeclaration,
                GlobalSearchScope.projectScope(myProject)
            )
        )
        updateSettingsPreferences()
    }

    override fun getNumberOfOccurrences(): Int {
        return super.getNumberOfOccurrences(myFile)
    }

    companion object {

        val refactoringName: @DialogTitle String
            get() = "Inline File"
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinefile

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiUtilBase.getElementAtCaret
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.refactoring.inlinecommand.LatexInlineHandler
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.Log
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.parser.firstParentOfType

/**
 * Allows for inlining an include command. Does not do the inlining itself.
 *
 * @author jojo2357
 */
class LatexInlineFileHandler : LatexInlineHandler() {

    override fun canInlineElement(element: PsiElement?): Boolean {
        return canInlineLatexElement(element)
    }

    override fun inlineElement(project: Project, editor: Editor?, element: PsiElement) {
        // Resolve the file to be inlined
        val inlineFile: LatexFile = resolveInlineFile(element) ?: return

        val dialog = LatexInlineFileDialog(
            project,
            inlineFile,
            getReference(element, editor),
            editor != null
        )

        if (dialog.getNumberOfOccurrences() > 0) {
            if (ApplicationManager.getApplication().isUnitTestMode) {
                try {
                    dialog.doAction()
                }
                finally {
                    dialog.close(DialogWrapper.OK_EXIT_CODE, true)
                }
            }
            else {
                dialog.show()
            }
        }
        else {
            Notification(
                "LaTeX",
                "No usages found",
                "Could not find any usages for ${inlineFile.name}",
                NotificationType.ERROR
            ).notify(project)
        }
    }

    companion object {

        /**
         * This is static only so that the unit tests can use it, and also since it can be static
         */
        fun canInlineLatexElement(element: PsiElement?): Boolean {
            return when (element) {
                is LatexFile -> {
                    element.containingFile.isLatexFile()
                }

                is LatexCommands -> {
                    (element.references.filterIsInstance<InputFileReference>().isNotEmpty())
                }

                else -> false
            }
        }

        fun resolveInlineFile(element: PsiElement) = when (element) {
            // If we tried to inline the file in file view, or the argument in `input`
            is LatexFile -> element
            // If the caret was on the `input` command
            is LatexCommands -> {
                element.references.filterIsInstance<InputFileReference>()[0].resolve(false) as? LatexFile
            }

            else -> null
        }

        /**
         * Resolves the command that the inlining was called on. If called from file view then expected return is null
         */
        fun getReference(
            element: PsiElement,
            editor: Editor?
        ): PsiElement? {
            return if (element is LatexCommands)
                element
            else if (editor != null) {
                val ref: LatexCommands? = getElementAtCaret(editor)?.firstParentOfType(LatexCommands::class)
                if (ref != null)
                    ref
                else {
                    Log.warn("Could not find the command element for " + element.text)
                    null
                }
            }
            else null
        }
    }
}

package nl.hannahsten.texifyidea.refactoring.inlinefile

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.SearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.refactoring.BaseRefactoringProcessor
import com.intellij.usageView.UsageInfo
import com.intellij.usageView.UsageViewDescriptor
import nl.hannahsten.texifyidea.file.LatexFile

/**
 * Recieves settings and a target and performs the requested refactoring
 *
 * @see com.intellij.refactoring.inline.InlineMethodProcessor
 *
 * @author jojo2357
 */
class LatexInlineFileProcessor(
    myProject: Project,
    private val inlineFile: LatexFile,
    private val originalReference: PsiElement?,
    private val isInlineThisOnly: Boolean,
    private val isKeepTheDeclaration: Boolean,
    private val myScope: SearchScope = GlobalSearchScope.projectScope(myProject)
) : BaseRefactoringProcessor(myProject) {

    override fun createUsageViewDescriptor(usages: Array<out UsageInfo>): UsageViewDescriptor {
        return LatexInlineFileDescriptor(inlineFile)
    }

    override fun findUsages(): Array<UsageInfo> {
        val tempreferences = ReferencesSearch.search(inlineFile, myScope).findAll().asSequence()

        return tempreferences
            .distinct()
            .map(PsiReference::getElement)
            .map(::UsageInfo)
            .toList()
            .toTypedArray()
    }

    override fun performRefactoring(usages: Array<out UsageInfo>) {
        if (isInlineThisOnly) {
            if (originalReference != null)
                replaceUsage(originalReference)
            else if (!ApplicationManager.getApplication().isUnitTestMode)
                throw IllegalStateException("Inline this requested with no original reference (" + usages.size + ")")
            // else we are probably trying to elicit no response because this is illegal
        }
        else if (usages.isNotEmpty()) {
            ApplicationManager.getApplication().runWriteAction {
                CommandProcessor.getInstance().executeCommand(myProject, {
                    for (replaceUsage in usages) {
                        val replacereference = replaceUsage.element ?: continue
                        replaceUsage(replacereference)
                    }
                    if (!isKeepTheDeclaration)
                        inlineFile.delete()
                }, commandName, "Texify")
            }
        }
    }

    /**
     * Does the lifting by...replacing usages with the entirety of the file to be inlined
     *
     * @param psiElement The element to remove and replace with the contents of the file
     */
    private fun replaceUsage(psiElement: PsiElement) {
        val root = psiElement.replace(
            inlineFile.children[0]
        )

        for (i in 1 until inlineFile.children.size) {
            psiElement.containingFile.addAfter(inlineFile.children[i], root)
        }
    }

    override fun getCommandName(): String {
        return "Inlining File " + this.inlineFile.name
    }
}

package nl.hannahsten.texifyidea.remotelibraries.mendeley

import com.intellij.credentialStore.Credentials
import com.intellij.ide.passwordSafe.PasswordSafe
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.jetty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import nl.hannahsten.texifyidea.util.CredentialAttributes.Mendeley.refreshTokenAttributes
import nl.hannahsten.texifyidea.util.CredentialAttributes.Mendeley.tokenAttributes

/**
 * Authorization via OAuth:
 * - Get authentication code token by letting the user log in via a browser.
 * - Exchange the authentication code for an access token, store this access token and the refresh token so we can make
 *   future requests on behalf of the user.
 * - On a future request, use the access token if it is still valid, otherwise use the refresh token to get a new access token.
 */
object MendeleyAuthenticator {

    init {
        createAuthenticationServer()
    }

    /**
     * The port is fixed by Mendeley, so we choose a port that hopefully no one has anything running on.
     */
    private const val port = 59473

    private const val redirectPath = "/"

    private const val redirectUrl = "http://localhost:$port$redirectPath"

    /**
     * See [Mendeley documentation](https://dev.mendeley.com/reference/topics/authorization_auth_code.html) for explanations
     * about these parameters.
     */
    private val authorizationParameters: String = Parameters.build {
        append("client_id", MendeleyCredentials.id)
        append("redirect_uri", redirectUrl)
        append("response_type", "code")
        append("scope", "all")
    }.formUrlEncode()

    val authorizationUrl = "https://api.mendeley.com/oauth/authorize?$authorizationParameters"

    /**
     * This server is a var because a stopped server cannot be restarted, so a new server is created before every
     * authentication attempt.
     *
     * @see [createAuthenticationServer]
     */
    private lateinit var authenticationServer: JettyApplicationEngine

    var isUserAuthenticationFinished = false

    /**
     * Authentication code that can be exchanged for an access token.
     */
    private var authenticationCode: String? = null

    /**
     * Create a server that listens on the redirect url of the authorization request. Displays login confirmation to the user and
     * gets the authorization code from the response.
     */
    private fun createAuthenticationServer() {
        try {
            isUserAuthenticationFinished = false
            authenticationServer = embeddedServer(Jetty, port = port) {
                routing {
                    get("/") {
                        this.call.respondText("You are now logged in to Mendeley. Click OK to continue.")
                        isUserAuthenticationFinished = true
                        authenticationCode = call.parameters["code"]
                    }
                }
            }.start(false)
        }
        catch (e: Exception) {
            throw Exception("Something went wrong when initializing the Jetty Server", e)
        }
    }

    /**
     * Client that can make requests to Mendeley to retrieve access tokens.
     */
    private val authenticationClient by lazy {
        HttpClient(CIO) {
            install(ContentNegotiation) {
                json(
                    Json {
                        ignoreUnknownKeys = true
                    }
                )
            }
            install(HttpRequestRetry) {
                retryOnServerErrors(maxRetries = 3)
                exponentialDelay()
            }
        }
    }

    /**
     * Exchange the [authenticationCode] for an access token. Use the stored access token when available.
     */
    suspend fun getAccessToken(): Credentials? {
        return PasswordSafe.instance.get(tokenAttributes) ?: authenticationCode?.let {
            val token: AccessTokenInfo = authenticationClient.submitForm(
                url = "https://api.mendeley.com/oauth/token",
                formParameters = Parameters.build {
                    append("grant_type", "authorization_code")
                    append("code", it)
                    append("redirect_uri", redirectUrl)
                }
            ) {
                basicAuth(MendeleyCredentials.id, MendeleyCredentials.secret.decipher())
            }.body()

            token.getCredentials().first
        }
    }

    /**
     * Use the refresh token to renew the access token.
     */
    suspend fun refreshAccessToken(): BearerTokens? {
        val refreshToken = PasswordSafe.instance.getPassword(refreshTokenAttributes) ?: return null
        val token: AccessTokenInfo = authenticationClient.submitForm(
            url = "https://api.mendeley.com/oauth/token",
            formParameters = Parameters.build {
                append("grant_type", "refresh_token")
                append("refresh_token", refreshToken)
                append("redirect_uri", "http://localhost:$port/")
            }
        ) {
            basicAuth(MendeleyCredentials.id, MendeleyCredentials.secret.decipher())
        }.body()

        val (tokenCredentials, refreshTokenCredentials) = token.getCredentials()

        return BearerTokens(tokenCredentials.password.toString(), refreshTokenCredentials.password.toString())
    }

    /**
     * Destroy and recreate the authentication server.
     */
    fun reset() {
        authenticationServer.stop()
        createAuthenticationServer()
    }

    /**
     * Decipher the Mendeley secret...
     */
    private fun String.decipher() = toCharArray().map { it.minus(12) }.joinToString("")

    /**
     * Data class to deserialize Mendeley's response with the access token.
     */
    @Serializable
    data class AccessTokenInfo(
        @SerialName("access_token") val accessToken: String,
        @SerialName("expires_in") val expiresIn: Int,
        @SerialName("refresh_token") val refreshToken: String? = null,
        @SerialName("token_type") val tokenType: String,
    ) {

        fun getCredentials(): Pair<Credentials, Credentials> {
            val tokenCredentials = Credentials("token", accessToken)
            val refreshTokenCredentials = Credentials("refresh_token", refreshToken)

            PasswordSafe.instance.set(tokenAttributes, tokenCredentials)
            PasswordSafe.instance.set(refreshTokenAttributes, refreshTokenCredentials)

            return tokenCredentials to refreshTokenCredentials
        }
    }
}

package nl.hannahsten.texifyidea.remotelibraries.mendeley

object MendeleyCredentials {

    /**
     * TeXiFy id to communicate with Mendeley.
     */
    const val id = "13334"

    /**
     * TeXiFy secret to communicate with Mendeley.
     */
    const val secret = "xBa\u0083o\u0082VvmSx`qes?"
}

package nl.hannahsten.texifyidea.remotelibraries.mendeley

import com.intellij.ide.passwordSafe.PasswordSafe
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import nl.hannahsten.texifyidea.remotelibraries.RemoteBibLibrary
import nl.hannahsten.texifyidea.util.CredentialAttributes.Mendeley
import nl.hannahsten.texifyidea.util.paginateViaLinkHeader

/**
 * [Mendeley](https://www.mendeley.com/reference-manager/library/all-references/) library.
 */
class MendeleyLibrary(override val identifier: String = NAME, override val displayName: String = "Mendeley") :
    RemoteBibLibrary(identifier, displayName) {

    /**
     * Client to make regular Mendeley requests with. The client contains all information to be able to authorize a
     * request, so individual requests don't have to take this into account.
     */
    private val client by lazy {
        HttpClient(CIO) {
            install(Auth) {
                bearer {
                    // Attempt to load the access token from memory.
                    loadTokens {
                        val token = PasswordSafe.instance.getPassword(Mendeley.tokenAttributes) ?: return@loadTokens null
                        val refreshToken = PasswordSafe.instance.getPassword(Mendeley.refreshTokenAttributes) ?: return@loadTokens null
                        BearerTokens(token, refreshToken)
                    }
                    // If the access token was not in memory or was expired, refresh it using the refreshtoken.
                    refreshTokens {
                        return@refreshTokens MendeleyAuthenticator.refreshAccessToken()
                    }
                }
            }
            install(HttpRequestRetry) {
                retryOnServerErrors(maxRetries = 3)
                exponentialDelay()
            }
        }
    }

    override suspend fun getBibtexString(): Pair<HttpResponse, String> {
        MendeleyAuthenticator.getAccessToken()
        return client.get(urlString = "https://api.mendeley.com/documents") {
            header("Accept", "application/x-bibtex")
            parameter("view", "bib")
            parameter("limit", 50)
        }.paginateViaLinkHeader {
            client.get(it) {
                header("Accept", "application/x-bibtex")
            }
        }
    }

    override fun destroyCredentials() {
        PasswordSafe.instance.set(Mendeley.tokenAttributes, null)
        PasswordSafe.instance.set(Mendeley.refreshTokenAttributes, null)
    }

    companion object {

        const val NAME = "Mendeley"
    }
}

package nl.hannahsten.texifyidea.remotelibraries.state

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.project.ProjectManager
import com.intellij.psi.PsiErrorElement
import com.intellij.psi.PsiFileFactory
import com.intellij.util.xmlb.Converter
import nl.hannahsten.texifyidea.file.BibtexFileType
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.parser.childrenOfType

/**
 * Convert from String to [BibtexEntry], and the other way around, using PSI and the parser.
 * This implies that this converter has to be used from within a thread that has read access.
 */
class BibtexEntryListConverter : Converter<List<BibtexEntry>>() {

    override fun toString(value: List<BibtexEntry>): String {
        return value.joinToString("\n") { it.text }
    }

    override fun fromString(value: String): List<BibtexEntry> {
        val project = ProjectManager.getInstance().defaultProject
        val file = PsiFileFactory.getInstance(project).createFileFromText("DUMMY.bib", BibtexFileType, value)

        if (file.children.any { it is PsiErrorElement }) {
            Notification(
                "LaTeX",
                "Library could not be imported completely",
                "Some bib elements might be missing in the imported library. See the TeXiFy wiki for more information.",
                NotificationType.WARNING
            ).notify(project)
        }

        return file.childrenOfType<BibtexEntry>().toList()
    }
}

package nl.hannahsten.texifyidea.remotelibraries.state

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.openapi.application.runReadAction
import com.intellij.util.xmlb.Converter
import nl.hannahsten.texifyidea.psi.BibtexEntry

/**
 * Converter to serialize the library map from and to a string. We use Jackson to convert the map from and to XML. This
 * XML will be stored in a `value="<insert xml here>"` tag inside IntelliJs XML, so we end up with some form of nested XML. Fun.
 */
class LibraryStateConverter : Converter<Map<String, LibraryState>>() {

    @JsonIgnoreProperties(ignoreUnknown = true)
    data class LibraryWrapper(
        val displayName: String = "",
        val libraryType: Class<*> = Any::class.java,
        val bibtex: BibItems = BibItems()
    )

    data class BibItems(val items: List<BibtexEntry> = emptyList())

    override fun toString(value: Map<String, LibraryState>): String? {
        val module = JacksonXmlModule()

        module.addSerializer(
            BibItems::class.java,
            object : JsonSerializer<BibItems>() {
                override fun serialize(p0: BibItems, p1: JsonGenerator, p2: SerializerProvider) {
                    runReadAction { p1.writeString(BibtexEntryListConverter().toString(p0.items)) }
                }
            }
        )

        return XmlMapper(module)
            .writerWithDefaultPrettyPrinter()
            .writeValueAsString(value.mapValues { LibraryWrapper(it.value.displayName, it.value.libraryType, BibItems(it.value.entries)) })
    }

    override fun fromString(value: String): Map<String, LibraryState> {
        val module = JacksonXmlModule()

        module.addDeserializer(
            BibItems::class.java,
            object : JsonDeserializer<BibItems>() {
                override fun deserialize(p0: JsonParser, p1: DeserializationContext): BibItems {
                    return runReadAction { BibItems(BibtexEntryListConverter().fromString(p0.text)) }
                }
            }
        )

        return XmlMapper(module)
            .readValue<Map<String, LibraryWrapper>>(value)
            .mapValues { LibraryState(it.value.displayName, it.value.libraryType, it.value.bibtex.items) }
    }
}

package nl.hannahsten.texifyidea.remotelibraries.state

import com.intellij.util.xmlb.annotations.OptionTag
import nl.hannahsten.texifyidea.psi.BibtexEntry

/**
 * State that can read and write library contents to and from disk.
 *
 * This is (de)serialized with [LibraryStateConverter], which converts this class to xml. Therefore, the keys in [libraries]
 * should be valid xml tags, e.g., they must start with a letter and cannot contain colons (xml namespaces are not supported).
 */
data class RemoteLibraryState(
    @OptionTag(converter = LibraryStateConverter::class)
    var libraries: Map<String, LibraryState> = emptyMap()
)

/**
 * State of a library that stores all the necessary information to be able to reconstruct a library when loading it from disk.
 *
 * Note that user information (like passwords) is not stored here, but in the [password safe](https://plugins.jetbrains.com/docs/intellij/persisting-sensitive-data.html).
 */
data class LibraryState(
    /**
     * Store the display name of the library.
     */
    val displayName: String,
    /**
     * Store the type of the library, so we can reconstruct it when loading the state from disk.
     */
    val libraryType: Class<*>,
    /**
     * Store the entries from the library, so we don't have to query the libraries on startup.
     */
    var entries: List<BibtexEntry>
)

package nl.hannahsten.texifyidea.remotelibraries.zotero

import com.intellij.ide.passwordSafe.PasswordSafe
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import nl.hannahsten.texifyidea.remotelibraries.RemoteBibLibrary
import nl.hannahsten.texifyidea.util.CredentialAttributes
import nl.hannahsten.texifyidea.util.paginateViaLinkHeader

class ZoteroLibrary(override val identifier: String = NAME, override val displayName: String = "Zotero") :
    RemoteBibLibrary(identifier, displayName) {

    private val client by lazy {
        HttpClient(CIO) {
            install(HttpRequestRetry) {
                retryOnServerErrors(maxRetries = 3)
                exponentialDelay()
            }
        }
    }

    override suspend fun getBibtexString(): Pair<HttpResponse, String> {
        val credentials = PasswordSafe.instance.get(CredentialAttributes.Zotero.userAttributes)
        return client.get("$BASE_URL/users/${credentials?.userName}/items") {
            headers {
                append("Zotero-API-version", VERSION.toString())
                append("Zotero-API-key", credentials?.password.toString())
            }
            parameter("format", "bibtex")
            parameter("limit", PAGINATION_LIMIT)
        }.paginateViaLinkHeader {
            client.get(it) {
                headers {
                    append("Zotero-API-version", VERSION.toString())
                    append("Zotero-API-key", credentials?.password.toString())
                }
            }
        }
    }

    override fun destroyCredentials() {
        PasswordSafe.instance.set(CredentialAttributes.Zotero.userAttributes, null)
    }

    companion object {

        const val VERSION = 3
        const val BASE_URL = "https://api.zotero.org"
        const val PAGINATION_LIMIT = 50
        const val NAME = "Zotero"
    }
}

package nl.hannahsten.texifyidea.run.bibtex

import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment

/**
 * @author Sten Wessel
 */
open class BibtexCommandLineState(
    environment: ExecutionEnvironment,
    private val runConfig: BibtexRunConfiguration
) : CommandLineState(environment) {

    @Throws(ExecutionException::class)
    override fun startProcess(): ProcessHandler {
        val compiler = runConfig.compiler ?: throw ExecutionException("No valid compiler specified.")
        val command: List<String> = compiler.getCommand(runConfig, environment.project) ?: throw ExecutionException("Compile command could not be created.")

        // The working directory is as specified by the user in the working directory.
        // The fallback (if null or empty) directory is the directory of the main file.
        val bibPath = runConfig.bibWorkingDir?.path
        val commandLine = if (!bibPath.isNullOrBlank()) {
            GeneralCommandLine(command).withWorkDirectory(bibPath)
        }
        else GeneralCommandLine(command).withWorkDirectory(runConfig.mainFile?.parent?.path)

        val handler: ProcessHandler = KillableProcessHandler(commandLine.withEnvironment(runConfig.environmentVariables.envs))

        // Reports exit code to run output window when command is terminated
        ProcessTerminatedListener.attach(handler, environment.project)

        return handler
    }
}

package nl.hannahsten.texifyidea.run.bibtex

import com.intellij.diagnostic.logging.LogConsoleManagerBase
import com.intellij.execution.Executor
import com.intellij.execution.configuration.EnvironmentVariablesData
import com.intellij.execution.configurations.*
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.icons.AllIcons
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogTabComponent
import nl.hannahsten.texifyidea.run.compiler.BibliographyCompiler
import org.jdom.Element

/**
 * @author Sten Wessel
 */
class BibtexRunConfiguration(
    project: Project,
    factory: ConfigurationFactory,
    name: String
) : RunConfigurationBase<BibtexCommandLineState>(project, factory, name), LocatableConfiguration {

    companion object {

        private const val PARENT_ELEMENT = "texify-bibtex"
        private const val COMPILER = "compiler"
        private const val COMPILER_PATH = "compiler-path"
        private const val COMPILER_ARGUMENTS = "compiler-arguments"
        private const val MAIN_FILE = "main-file"
        private const val AUX_DIR = "aux-dir"
    }

    var compiler: BibliographyCompiler? = null
    var compilerPath: String? = null
    var compilerArguments: String? = null
        set(value) {
            field = value?.trim()
            field = if (field?.isEmpty() == true) null else field
        }
    var environmentVariables: EnvironmentVariablesData = EnvironmentVariablesData.DEFAULT

    var mainFile: VirtualFile? = null
    var bibWorkingDir: VirtualFile? = null

    override fun getConfigurationEditor(): SettingsEditor<out RunConfiguration> = BibtexSettingsEditor(project)

    override fun createAdditionalTabComponents(
        manager: AdditionalTabComponentManager?,
        startedProcess: ProcessHandler?
    ) {
        super.createAdditionalTabComponents(manager, startedProcess)

        if (manager is LogConsoleManagerBase && startedProcess != null) {
            manager.addAdditionalTabComponent(BibtexLogTabComponent(project, mainFile, startedProcess), "BibTeX-Log", AllIcons.Vcs.Changelist, false)
        }
    }

    override fun checkConfiguration() {
        if (compiler == null) {
            throw RuntimeConfigurationError("No compiler specified.")
        }

        if (mainFile == null) {
            throw RuntimeConfigurationError("No main file specified.")
        }
    }

    override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState {
        return BibtexCommandLineState(environment, this)
    }

    override fun readExternal(element: Element) {
        super<RunConfigurationBase>.readExternal(element)

        val parent = element.getChild(PARENT_ELEMENT)

        compiler = try {
            BibliographyCompiler.valueOf(parent.getChildText(COMPILER))
        }
        catch (e: IllegalArgumentException) {
            null
        }

        compilerPath = parent.getChildText(COMPILER_PATH)
        if (compilerPath?.isEmpty() == true) {
            compilerPath = null
        }

        compilerArguments = parent.getChildText(COMPILER_ARGUMENTS)
        environmentVariables = EnvironmentVariablesData.readExternal(parent)

        val mainFilePath = parent.getChildText(MAIN_FILE)
        mainFile = if (mainFilePath != null) {
            LocalFileSystem.getInstance().findFileByPath(mainFilePath)
        }
        else {
            null
        }

        val auxDirPath = parent.getChildText(AUX_DIR)
        bibWorkingDir = if (auxDirPath != null) {
            LocalFileSystem.getInstance().findFileByPath(auxDirPath)
        }
        else {
            null
        }
    }

    override fun writeExternal(element: Element) {
        super<RunConfigurationBase>.writeExternal(element)

        val parent = element.getChild(PARENT_ELEMENT) ?: Element(PARENT_ELEMENT).apply { element.addContent(this) }
        parent.removeContent()

        parent.addContent(Element(COMPILER).apply { text = compiler?.name ?: "" })
        parent.addContent(Element(COMPILER_PATH).apply { text = compilerPath ?: "" })
        parent.addContent(Element(COMPILER_ARGUMENTS).apply { text = compilerArguments ?: "" })
        this.environmentVariables.writeExternal(parent)
        parent.addContent(Element(MAIN_FILE).apply { text = mainFile?.path ?: "" })
        parent.addContent(Element(AUX_DIR).apply { text = bibWorkingDir?.path ?: "" })
    }

    override fun isGeneratedName() = name == suggestedName()

    override fun suggestedName() = mainFile?.nameWithoutExtension.plus(" bibliography")

    fun setSuggestedName() {
        name = suggestedName()
    }
}

package nl.hannahsten.texifyidea.run.bibtex

import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.ConfigurationType
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.run.latex.LatexConfigurationFactory
import javax.swing.Icon

/**
 * Describes a BibTeX run configuration.
 *
 * @author Sten Wessel
 */
class BibtexRunConfigurationType : ConfigurationType {

    override fun getIcon(): Icon = TexifyIcons.BUILD_BIB

    override fun getConfigurationTypeDescription() = "Build a BibTeX bibliography"

    override fun getId() = "BIBTEX_RUN_CONFIGURATION"

    override fun getDisplayName() = "BibTeX"

    override fun getConfigurationFactories(): Array<ConfigurationFactory> = arrayOf(LatexConfigurationFactory(this))
}

package nl.hannahsten.texifyidea.run.bibtex

import com.intellij.execution.configuration.EnvironmentVariablesComponent
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileTypeDescriptor
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.*
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.ui.RawCommandLineEditor
import com.intellij.ui.SeparatorComponent
import nl.hannahsten.texifyidea.run.compiler.BibliographyCompiler
import java.awt.event.ItemEvent
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JPanel

/**
 * @author Sten Wessel
 */
class BibtexSettingsEditor(private val project: Project) : SettingsEditor<BibtexRunConfiguration>() {

    private lateinit var panel: JPanel
    private lateinit var compiler: LabeledComponent<ComboBox<BibliographyCompiler>>
    private lateinit var enableCompilerPath: JCheckBox
    private lateinit var compilerPath: TextFieldWithBrowseButton
    private lateinit var compilerArguments: LabeledComponent<RawCommandLineEditor>
    private lateinit var environmentVariables: EnvironmentVariablesComponent
    private lateinit var mainFile: LabeledComponent<TextFieldWithBrowseButton>

    /** Keep track of the the working directory for bibtex, i.e., where bibtex should find the files it needs.
     * Is automatically set based on the LaTeX run config when created. */
    private lateinit var bibWorkingDir: LabeledComponent<TextFieldWithBrowseButton>

    override fun createEditor(): JComponent {
        createUIComponents()
        return panel
    }

    override fun resetEditorFrom(runConfig: BibtexRunConfiguration) {
        compiler.component.selectedItem = runConfig.compiler
        compilerPath.text = runConfig.compilerPath ?: ""
        compilerArguments.component.text = runConfig.compilerArguments ?: ""
        environmentVariables.envData = runConfig.environmentVariables
        enableCompilerPath.isSelected = runConfig.compilerPath != null
        mainFile.component.text = runConfig.mainFile?.path ?: ""
        bibWorkingDir.component.text = runConfig.bibWorkingDir?.path ?: ""
    }

    override fun applyEditorTo(runConfig: BibtexRunConfiguration) {
        runConfig.compiler = compiler.component.selectedItem as BibliographyCompiler?
        runConfig.compilerPath = if (enableCompilerPath.isSelected) compilerPath.text else null
        runConfig.compilerArguments = compilerArguments.component.text
        runConfig.environmentVariables = environmentVariables.envData
        runConfig.mainFile = LocalFileSystem.getInstance().findFileByPath(mainFile.component.text)
        runConfig.bibWorkingDir = LocalFileSystem.getInstance().findFileByPath(bibWorkingDir.component.text)
    }

    private fun createUIComponents() {
        panel = JPanel().apply {
            layout = VerticalFlowLayout(VerticalFlowLayout.TOP)

            // Compiler
            val compilerField = ComboBox(BibliographyCompiler.values())
            compiler = LabeledComponent.create(compilerField, "Compiler")
            add(compiler)

            // Custom compiler path
            compilerPath = TextFieldWithBrowseButton().apply {
                addBrowseFolderListener(
                    TextBrowseFolderListener(
                        FileChooserDescriptor(true, false, false, false, false, false)
                            .withFileFilter { file -> file.nameWithoutExtension == (compiler.component.selectedItem as BibliographyCompiler?)?.executableName }
                            .withTitle("Choose ${compiler.component.selectedItem} executable")
                    )
                )

                isEnabled = false
                addPropertyChangeListener("enabled") { e ->
                    if (!(e.newValue as Boolean)) {
                        this.text = ""
                    }
                }
            }

            enableCompilerPath = JCheckBox("Select custom compiler executable path (required on Mac OS X)").apply {
                addItemListener { e ->
                    compilerPath.isEnabled = e.stateChange == ItemEvent.SELECTED
                }
            }

            add(enableCompilerPath)
            add(compilerPath)

            // Custom compiler arguments
            val argumentsTitle = "Custom compiler arguments"
            val argumentsField = RawCommandLineEditor()
            compilerArguments = LabeledComponent.create(argumentsField, argumentsTitle)
            add(compilerArguments)

            environmentVariables = EnvironmentVariablesComponent()
            add(environmentVariables)

            add(SeparatorComponent())

            // Main file
            val mainFileField = TextFieldWithBrowseButton().apply {
                addBrowseFolderListener(
                    TextBrowseFolderListener(
                        FileTypeDescriptor("Choose the Main .tex File", ".tex")
                            .withRoots(*ProjectRootManager.getInstance(project).contentRootsFromAllModules)
                    )
                )
            }
            mainFile = LabeledComponent.create(mainFileField, "Main file that includes bibliography")
            add(mainFile)

            // Working directory
            val workingDirField = TextFieldWithBrowseButton()
            workingDirField.addBrowseFolderListener(
                TextBrowseFolderListener(
                    FileChooserDescriptor(false, true, false, false, false, false)
                        .withTitle("Choose the BibTeX Working Directory")
                        .withRoots(
                            *ProjectRootManager.getInstance(project)
                                .contentRootsFromAllModules
                        )
                )
            )
            bibWorkingDir = LabeledComponent.create(workingDirField, "Working directory for bibtex")
            add(bibWorkingDir)
        }
    }
}

package nl.hannahsten.texifyidea.run.bibtex

import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.impl.RunConfigurationBeforeRunProvider
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.util.Key
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration

/**
 * @author Sten Wessel
 */
class RunBibtexListener(
    private val bibtexSettings: RunnerAndConfigurationSettings,
    private val latexConfiguration: LatexRunConfiguration,
    private val environment: ExecutionEnvironment,
    private val runLatexAfterwards: Boolean = true
) : ProcessListener {

    override fun processTerminated(event: ProcessEvent) {
        if (event.exitCode != 0) {
            return
        }

        // Run bibtex compiler (blocking execution)
        if (!RunConfigurationBeforeRunProvider.doExecuteTask(environment, bibtexSettings, null)) {
            return
        }

        if (runLatexAfterwards) {
            // Mark the next latex runs to exclude bibtex compilation
            latexConfiguration.isFirstRunConfig = false
            try {
                val latexSettings = RunManagerImpl.getInstanceImpl(environment.project).getSettings(latexConfiguration)
                    ?: return

                // Compile twice to fix references etc
                // Mark the next latex run as not being the final one, to avoid for instance opening the pdf viewer too early (with possible multiple open pdfs as a result, or a second open would fail because of a write lock)
                latexConfiguration.isLastRunConfig = false
                RunConfigurationBeforeRunProvider.doExecuteTask(environment, latexSettings, null)
                latexConfiguration.isLastRunConfig = true
                RunConfigurationBeforeRunProvider.doExecuteTask(environment, latexSettings, null)
            }
            finally {
                latexConfiguration.isLastRunConfig = false
                latexConfiguration.isFirstRunConfig = true
            }
        }
    }

    override fun onTextAvailable(p0: ProcessEvent, p1: Key<*>) {
        // Do nothing.
    }

    override fun processWillTerminate(p0: ProcessEvent, p1: Boolean) {
        // Do nothing.
    }

    override fun startNotified(p0: ProcessEvent) {
        // Do nothing.
    }
}

package nl.hannahsten.texifyidea.run.compiler

import com.intellij.openapi.project.Project
import com.intellij.util.execution.ParametersListUtil
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration

/**
 * @author Thomas Schouten
 */
internal object BiberCompiler : Compiler<BibtexRunConfiguration> {

    override val displayName = "Biber"
    override val executableName = "biber"

    override fun getCommand(runConfig: BibtexRunConfiguration, project: Project): List<String>? = mutableListOf<String>().apply {
        add(runConfig.compilerPath ?: executableName)

        // Biber can find auxiliary files, but the flag is different from bibtex.
        // The following flag assumes the command is executed in the directory where the .bcf control file is.
        // The extra directory added is the directory from which the path to the .bib resource file is searched as specified in the .bcf file.
        add("--input-directory=${runConfig.mainFile?.parent?.path ?: ""}")

        runConfig.compilerArguments?.let { addAll(ParametersListUtil.parse(it)) }

        add(runConfig.mainFile?.nameWithoutExtension ?: return null)
    }
}

package nl.hannahsten.texifyidea.run.compiler

import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration

/**
 * @author Hannah Schellekens, Sten Wessel
 */
enum class BibliographyCompiler(private val compiler: Compiler<BibtexRunConfiguration>) : Compiler<BibtexRunConfiguration> by compiler {

    BIBTEX(BibtexCompiler),
    BIBER(BiberCompiler);

    override fun toString() = displayName
}

package nl.hannahsten.texifyidea.run.compiler

import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.util.execution.ParametersListUtil
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil

/**
 * @author Sten Wessel
 */
internal object BibtexCompiler : Compiler<BibtexRunConfiguration> {

    override val displayName = "BibTeX"
    override val executableName = "bibtex"

    override fun getCommand(runConfig: BibtexRunConfiguration, project: Project): List<String>? {
        val command = mutableListOf<String>()

        val moduleRoots = ProjectRootManager.getInstance(project).contentSourceRoots

        command.apply {
            add(runConfig.compilerPath ?: executableName)

            runConfig.compilerArguments?.let { addAll(ParametersListUtil.parse(it)) }

            // Include files from auxiliary directory on Windows
            if (LatexSdkUtil.isMiktexAvailable) {
                add("-include-directory=${runConfig.mainFile?.parent?.path ?: ""}")
                addAll(moduleRoots.map { "-include-directory=${it.path}" })
            }

            add(runConfig.mainFile?.nameWithoutExtension ?: return null)
        }

        return command.toList()
    }
}

package nl.hannahsten.texifyidea.run.compiler

import com.intellij.execution.configurations.RunConfiguration
import com.intellij.openapi.project.Project

/**
 * @author Sten Wessel
 */
interface Compiler<in R : RunConfiguration> {

    val displayName: String
    val executableName: String

    fun getCommand(runConfig: R, project: Project): List<String>?
}

package nl.hannahsten.texifyidea.run.compiler

/**
 * Other external tools which have to run between, before or after LaTeX runs, and that are not [MakeindexProgram]s or [BibtexCompiler]s.
 *
 * @author Thomas Schouten
 */
enum class ExternalTool(val executableName: String) {

    PYTHONTEX("pythontex")
}

package nl.hannahsten.texifyidea.run.compiler

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.execution.ParametersListUtil
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.settings.sdk.DockerSdk
import nl.hannahsten.texifyidea.settings.sdk.DockerSdkAdditionalData
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import nl.hannahsten.texifyidea.util.LatexmkRcFileFinder
import nl.hannahsten.texifyidea.util.runCommand
import java.util.*

/**
 * @author Hannah Schellekens, Sten Wessel
 */
@Suppress("DuplicatedCode")
enum class LatexCompiler(private val displayName: String, val executableName: String) {

    PDFLATEX("pdfLaTeX", "pdflatex") {

        override fun createCommand(
            runConfig: LatexRunConfiguration,
            auxilPath: String?,
            outputPath: String?,
            moduleRoot: VirtualFile?,
            moduleRoots: Array<VirtualFile>
        ): MutableList<String> {
            // For now only support custom executable for TeX Live
            // At least avoids prepending a full path to a supposed TeX Live executable when in fact it will be prepended by a docker command
            val executable = LatexSdkUtil.getExecutableName(executableName, runConfig.project, runConfig.getLatexDistributionType())
            val command = mutableListOf(runConfig.compilerPath ?: executable)

            command.add("-file-line-error")
            command.add("-interaction=nonstopmode")
            command.add("-synctex=1")
            command.add("-output-format=${runConfig.outputFormat.name.lowercase(Locale.getDefault())}")

            command.add("-output-directory=$outputPath")

            // -aux-directory only exists on MiKTeX
            if (auxilPath != null && runConfig.getLatexDistributionType().isMiktex(project = runConfig.project)) {
                command.add("-aux-directory=$auxilPath")
            }

            // Prepend root paths to the input search path
            if (runConfig.getLatexDistributionType().isMiktex(runConfig.project)) {
                moduleRoots.forEach {
                    command.add("-include-directory=${it.path}")
                }
            }

            return command
        }
    },

    LUALATEX("LuaLaTeX", "lualatex") {

        override fun createCommand(
            runConfig: LatexRunConfiguration,
            auxilPath: String?,
            outputPath: String?,
            moduleRoot: VirtualFile?,
            moduleRoots: Array<VirtualFile>
        ): MutableList<String> {
            val command = mutableListOf(
                runConfig.compilerPath ?: LatexSdkUtil.getExecutableName(
                    executableName,
                    runConfig.project,
                    runConfig.getLatexDistributionType()
                )
            )

            // Some commands are the same as for pdflatex
            command.add("-file-line-error")
            command.add("-interaction=nonstopmode")
            command.add("-synctex=1")
            command.add("-output-format=${runConfig.outputFormat.name.lowercase(Locale.getDefault())}")

            command.add("-output-directory=$outputPath")

            // Note that lualatex has no -aux-directory
            return command
        }
    },

    LATEXMK("Latexmk", "latexmk") {

        override val includesBibtex = true

        override val includesMakeindex = true

        override val handlesNumberOfCompiles = true

        override val outputFormats = arrayOf(Format.DEFAULT, Format.PDF, Format.DVI)

        override fun createCommand(
            runConfig: LatexRunConfiguration,
            auxilPath: String?,
            outputPath: String?,
            moduleRoot: VirtualFile?,
            moduleRoots: Array<VirtualFile>
        ): MutableList<String> {
            val command = mutableListOf(
                runConfig.compilerPath ?: LatexSdkUtil.getExecutableName(
                    executableName,
                    runConfig.project,
                    runConfig.getLatexDistributionType()
                )
            )

            val isLatexmkRcFilePresent = LatexmkRcFileFinder.isLatexmkRcFilePresent(runConfig)

            // If it is present, assume that it will handle everything (command line options would overwrite latexmkrc options)
            if (!isLatexmkRcFilePresent) {
                // Adding the -pdf flag makes latexmk run with pdflatex, which is definitely preferred over running with just latex
                command.add("-pdf")
                command.add("-file-line-error")
                command.add("-interaction=nonstopmode")
                command.add("-synctex=1")
            }

            if (runConfig.outputFormat != Format.DEFAULT) {
                command.add("-output-format=${runConfig.outputFormat.name.lowercase(Locale.getDefault())}")
            }

            command.add("-output-directory=$outputPath")

            if (auxilPath != null && runConfig.getLatexDistributionType().isMiktex(runConfig.project)) {
                command.add("-aux-directory=$auxilPath")
            }

            // -include-directory does not work with latexmk
            return command
        }
    },

    XELATEX("XeLaTeX", "xelatex") {

        override val outputFormats = arrayOf(Format.PDF, Format.XDV)

        override fun createCommand(
            runConfig: LatexRunConfiguration,
            auxilPath: String?,
            outputPath: String?,
            moduleRoot: VirtualFile?,
            moduleRoots: Array<VirtualFile>
        ): MutableList<String> {
            val command = mutableListOf(
                runConfig.compilerPath ?: LatexSdkUtil.getExecutableName(
                    executableName,
                    runConfig.project,
                    runConfig.getLatexDistributionType()
                )
            )

            // As usual, available command line options can be viewed with xelatex --help
            // On TeX Live, installing collection-xetex should be sufficient to get xelatex
            command.add("-file-line-error")
            command.add("-interaction=nonstopmode")
            command.add("-synctex=1")

            if (runConfig.outputFormat == Format.XDV) {
                command.add("-no-pdf")
            }

            command.add("-output-directory=$outputPath")

            if (auxilPath != null && runConfig.getLatexDistributionType().isMiktex(runConfig.project)) {
                command.add("-aux-directory=$auxilPath")
            }

            // Prepend root paths to the input search path
            if (runConfig.getLatexDistributionType().isMiktex(runConfig.project)) {
                moduleRoots.forEach {
                    command.add("-include-directory=${it.path}")
                }
            }

            return command
        }
    },

    TEXLIVEONFLY("Texliveonfly", "texliveonfly") {

        override val outputFormats = arrayOf(Format.PDF)

        override fun createCommand(
            runConfig: LatexRunConfiguration,
            auxilPath: String?,
            outputPath: String?,
            moduleRoot: VirtualFile?,
            moduleRoots: Array<VirtualFile>
        ): MutableList<String> {
            val command = mutableListOf(
                runConfig.compilerPath ?: LatexSdkUtil.getExecutableName(
                    executableName,
                    runConfig.project,
                    runConfig.getLatexDistributionType()
                )
            )

            // texliveonfly is a Python script which calls other compilers (by default pdflatex), main feature is downloading packages automatically
            // commands can be passed to those compilers with the arguments flag, however apparently IntelliJ cannot handle quotes so we cannot pass multiple arguments to pdflatex.
            // Fortunately, -synctex=1 and -interaction=nonstopmode are on by default in texliveonfly
            // Since adding one will work without any quotes, we choose the output directory.
            if (outputPath != null) {
                command.add("--arguments=--output-directory=$outputPath")
            }

            return command
        }
    },

    TECTONIC("Tectonic", "tectonic") {

        override val includesBibtex = true

        override val handlesNumberOfCompiles = true

        override val outputFormats = arrayOf(Format.PDF, Format.HTML, Format.XDV, Format.AUX)

        override fun createCommand(
            runConfig: LatexRunConfiguration,
            auxilPath: String?,
            outputPath: String?,
            moduleRoot: VirtualFile?,
            moduleRoots: Array<VirtualFile>
        ): MutableList<String> {
            // The available command line arguments can be found at https://github.com/tectonic-typesetting/tectonic/blob/d7a8497c90deb08b5e5792a11d6e8b082f53bbb7/src/bin/tectonic.rs#L158
            val command = mutableListOf(runConfig.compilerPath ?: executableName)

            command.add("--synctex")

            command.add("--outfmt=${runConfig.outputFormat.name.lowercase(Locale.getDefault())}")

            if (outputPath != null) {
                command.add("--outdir=$outputPath")
            }

            return command
        }
    },

    ARARA("Arara", "arara") {

        override val includesBibtex = true

        override val handlesNumberOfCompiles = true

        override val outputFormats = arrayOf(Format.PDF)

        override fun createCommand(
            runConfig: LatexRunConfiguration,
            auxilPath: String?,
            outputPath: String?,
            moduleRoot: VirtualFile?,
            moduleRoots: Array<VirtualFile>
        ): MutableList<String> {
            // Arara handles everything as configured by magic comments in the file.
            // We cannot use --verbose because it relies on user input
            return mutableListOf(runConfig.compilerPath ?: executableName)
        }
    },
    ;

    /**
     * Convert Windows paths to WSL paths.
     */
    private fun String.toPath(runConfig: LatexRunConfiguration): String =
        if (runConfig.getLatexDistributionType() == LatexDistributionType.WSL_TEXLIVE) {
            runCommand("wsl", "wslpath", "-a", this) ?: this
        }
        else this

    /**
     * Get the execution command for the latex compiler.
     *
     * @param runConfig
     *          The run configuration object to get the command for.
     * @param project
     *          The current project.
     */
    fun getCommand(runConfig: LatexRunConfiguration, project: Project): List<String>? {
        val rootManager = ProjectRootManager.getInstance(project)
        val fileIndex = rootManager.fileIndex
        val mainFile = runConfig.mainFile ?: return null
        val moduleRoot = fileIndex.getContentRootForFile(mainFile)
        // For now we disable module roots with Docker
        // Could be improved by mounting them to the right directory
        val moduleRoots = if (runConfig.getLatexDistributionType() != LatexDistributionType.DOCKER_MIKTEX) {
            rootManager.contentSourceRoots
        }
        else {
            emptyArray()
        }

        // If we used /miktex/work/out, an out directory would appear in the src folder on the host system
        val dockerOutputDir = "/miktex/out"
        val dockerAuxilDir = "/miktex/auxil"
        val outputPath = if (runConfig.getLatexDistributionType() != LatexDistributionType.DOCKER_MIKTEX) {
            runConfig.outputPath.getAndCreatePath()?.path?.toPath(runConfig)
        }
        else {
            dockerOutputDir
        }

        val auxilPath = if (runConfig.getLatexDistributionType() != LatexDistributionType.DOCKER_MIKTEX) {
            runConfig.auxilPath.getAndCreatePath()?.path?.toPath(runConfig)
        }
        else {
            dockerAuxilDir
        }

        val command = createCommand(
            runConfig,
            auxilPath,
            outputPath,
            moduleRoot,
            moduleRoots
        )

        if (runConfig.getLatexDistributionType() == LatexDistributionType.WSL_TEXLIVE) {
            var wslCommand = GeneralCommandLine(command).commandLineString

            // Custom compiler arguments specified by the user
            runConfig.compilerArguments?.let { arguments ->
                ParametersListUtil.parse(arguments)
                    .forEach { wslCommand += " $it" }
            }

            wslCommand += " ${mainFile.path.toPath(runConfig)}"

            return mutableListOf("bash", "-ic", wslCommand)
        }

        if (runConfig.getLatexDistributionType() == LatexDistributionType.DOCKER_MIKTEX) {
            createDockerCommand(runConfig, dockerAuxilDir, dockerOutputDir, mainFile, command)
        }

        // Custom compiler arguments specified by the user
        runConfig.compilerArguments?.let { arguments ->
            ParametersListUtil.parse(arguments)
                .forEach { command.add(it) }
        }

        command.add(mainFile.name)

        return command
    }

    @Suppress("SameParameterValue")
    private fun createDockerCommand(runConfig: LatexRunConfiguration, dockerAuxilDir: String, dockerOutputDir: String, mainFile: VirtualFile, command: MutableList<String>) {
        // See https://hub.docker.com/r/miktex/miktex
        "docker volume create --name miktex".runCommand()

        // Find the sdk corresponding to the type the user has selected in the run config
        val sdk = ProjectJdkTable.getInstance().allJdks.firstOrNull { it.sdkType is DockerSdk }

        val parameterList = mutableListOf(
            if (sdk == null) "docker" else (sdk.sdkType as DockerSdk).getExecutableName("docker", sdk.homePath!!),
            "run",
            "--rm",
            "-v",
            "miktex:/miktex/.miktex",
            "-v",
            "${mainFile.parent.path}:/miktex/work"
        )

        // Avoid mounting the mainfile parent also to /miktex/work/out,
        // because there may be a good reason to make the output directory the same as the source directory
        if (runConfig.outputPath.getAndCreatePath() != mainFile.parent) {
            parameterList.addAll(listOf("-v", "${runConfig.outputPath.getAndCreatePath()?.path}:$dockerOutputDir"))
        }

        if (runConfig.auxilPath.getAndCreatePath() != mainFile.parent) {
            parameterList.addAll(listOf("-v", "${runConfig.auxilPath.getAndCreatePath()?.path}:$dockerAuxilDir"))
        }

        parameterList.add((sdk?.sdkAdditionalData as? DockerSdkAdditionalData)?.imageName ?: "miktex:latest")

        command.addAll(0, parameterList)
    }

    /**
     * Create the command to execute to use the compiler.
     *
     * @param runConfig LaTeX run configuration which initiated the action of creating this command.
     * @param moduleRoot Module root.
     * @param moduleRoots List of source roots.
     *
     * @return The command to be executed.
     */
    protected open fun createCommand(
        runConfig: LatexRunConfiguration,
        auxilPath: String?,
        outputPath: String?,
        moduleRoot: VirtualFile?,
        moduleRoots: Array<VirtualFile>
    ): MutableList<String> = error("Not implemented for $this")

    /**
     * Whether the compiler includes running bibtex/biber.
     */
    open val includesBibtex = false

    /**
     * Whether the compiler includes running index programs.
     */
    open val includesMakeindex = false

    /**
     * Whether the compiler automatically determines the number of compiles needed.
     */
    open val handlesNumberOfCompiles = false

    /**
     * List of output formats supported by this compiler.
     */
    open val outputFormats: Array<Format> = arrayOf(Format.PDF, Format.DVI)

    override fun toString() = this.displayName

    companion object {

        fun byExecutableName(exe: String): LatexCompiler {
            return values().firstOrNull {
                it.executableName.equals(exe, true)
            } ?: PDFLATEX
        }
    }

    /**
     * @author Hannah Schellekens
     */
    enum class Format {

        DEFAULT, // Means: don't overwite the default, e.g. a default from the latexmkrc, i.e. don't add any command line parameters
        PDF,
        DVI,
        HTML,
        XDV,
        AUX;

        companion object {

            fun byNameIgnoreCase(name: String?): Format {
                return values().firstOrNull {
                    it.name.equals(name, ignoreCase = true)
                } ?: PDF
            }
        }
    }
}

package nl.hannahsten.texifyidea.run.compiler

/**
 * Makeindex programs (to create a .ind file).
 *
 * @author Thomas Schouten
 */
enum class MakeindexProgram(val executableName: String) {

    XINDY("texindy"),
    TRUEXINDY("xindy"), // requires perl
    MAKEINDEX("makeindex"),

    // Wrapper which calls makeindex or xindy, from the glossaries package (so technically it doesn't create an index but a glossary)
    MAKEGLOSSARIES("makeglossaries"), // requires perl
    MAKEGLOSSARIESLITE("makeglossaries-lite"), // makeglossaries without using perl
    BIB2GLS("bib2gls")
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.editor.autocompile.AutoCompileDoneListener
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.run.FileCleanupListener
import nl.hannahsten.texifyidea.run.OpenCustomPdfViewerListener
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration
import nl.hannahsten.texifyidea.run.bibtex.RunBibtexListener
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import nl.hannahsten.texifyidea.run.latex.externaltool.RunExternalToolListener
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer
import nl.hannahsten.texifyidea.run.linuxpdfviewer.ViewerForwardSearch
import nl.hannahsten.texifyidea.run.makeindex.RunMakeindexListener
import nl.hannahsten.texifyidea.run.pdfviewer.ExternalPdfViewer
import nl.hannahsten.texifyidea.run.sumatra.SumatraAvailabilityChecker
import nl.hannahsten.texifyidea.run.sumatra.SumatraForwardSearchListener
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.magic.PackageMagic
import java.io.File

/**
 * Run the run configuration: start the compile process and initiate forward search (when applicable).
 *
 * @author Sten Wessel
 */
open class LatexCommandLineState(environment: ExecutionEnvironment, private val runConfig: LatexRunConfiguration) : CommandLineState(environment) {

    @Throws(ExecutionException::class)
    override fun startProcess(): ProcessHandler {
        val compiler = runConfig.compiler ?: throw ExecutionException("No valid compiler specified.")
        val mainFile = runConfig.mainFile ?: throw ExecutionException("Main file is not specified.")

        // If the outdirs do not exist, we assume this is because either something went wrong and an incorrect output path was filled in,
        // or the user did not create a new project, for example by opening or importing existing resources,
        // so they still need to be created.
        if (runConfig.outputPath.virtualFile == null) {
            runConfig.outputPath.getAndCreatePath()
        }

        if (!runConfig.hasBeenRun) {
            // Show to the user what we're doing that takes so long (up to 30 seconds for a large project)
            // Unfortunately, this will block the UI (so you can't cancel it either), and I don't know how to run it in the background (e.g. Backgroundable) while still returning a ProcessHandler at the end of this method. Maybe it should be its own process.
            ProgressManager.getInstance().runProcessWithProgressSynchronously(
                { firstRunSetup(compiler) },
                "Generating Run Configuration...",
                false,
                runConfig.project
            )
        }

        val createdOutputDirectories = if (!runConfig.getLatexDistributionType().isMiktex(runConfig.project)) {
            runConfig.outputPath.updateOutputSubDirs()
        }
        else {
            setOf()
        }

        val handler = createHandler(mainFile, compiler)
        val isMakeindexNeeded = runMakeindexIfNeeded(handler, mainFile, runConfig.filesToCleanUp)
        runExternalToolsIfNeeded(handler)
        runConfig.hasBeenRun = true

        if (!isLastCompile(isMakeindexNeeded, handler)) return handler
        scheduleBibtexRunIfNeeded(handler)
        schedulePdfViewerIfNeeded(handler)
        scheduleFileCleanup(runConfig.filesToCleanUp, createdOutputDirectories, handler)

        return handler
    }

    private fun createHandler(mainFile: VirtualFile, compiler: LatexCompiler): KillableProcessHandler {
        // Make sure to create the command after generating the bib run config (which might change the output path)
        val command: List<String> = compiler.getCommand(runConfig, environment.project)
            ?: throw ExecutionException("Compile command could not be created.")

        val commandLine = GeneralCommandLine(command).withWorkDirectory(mainFile.parent.path)
            .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
            .withEnvironment(runConfig.environmentVariables.envs)
        val handler = KillableProcessHandler(commandLine)

        // Reports exit code to run output window when command is terminated
        ProcessTerminatedListener.attach(handler, environment.project)

        return handler
    }

    /**
     * Do some long-running checks to generate the correct run configuration.
     */
    private fun firstRunSetup(compiler: LatexCompiler) {
        // Only at this moment we know the user really wants to run the run configuration, so only now we do the expensive check of
        // checking for bibliography commands
        if (runConfig.bibRunConfigs.isEmpty() && !compiler.includesBibtex) {
            // Generating a bib run config involves PSI access, which requires a read action.
            runReadAction { runConfig.generateBibRunConfig() }

            runConfig.bibRunConfigs.forEach {
                val bibSettings = it

                // Pass necessary latex run configurations settings to the bibtex run configuration.
                (bibSettings.configuration as? BibtexRunConfiguration)?.apply {
                    // Check if the aux, out, or src folder should be used as bib working dir.
                    // This involves a synchronous refreshAndFindFileByPath, and hence cannot be done in a read action
                    this.bibWorkingDir = runConfig.getAuxilDirectory()
                }
            }
        }
    }

    private fun runExternalToolsIfNeeded(
        handler: KillableProcessHandler
    ): Boolean {
        val isAnyExternalToolNeeded = if (!runConfig.hasBeenRun) {
            // This is a relatively expensive check
            RunExternalToolListener.getRequiredExternalTools(runConfig.mainFile, runConfig.project).isNotEmpty()
        }
        else {
            false
        }

        if (runConfig.isFirstRunConfig && (runConfig.externalToolRunConfigs.isNotEmpty() || isAnyExternalToolNeeded)) {
            handler.addProcessListener(RunExternalToolListener(runConfig, environment))
        }

        return isAnyExternalToolNeeded
    }

    private fun runMakeindexIfNeeded(handler: KillableProcessHandler, mainFile: VirtualFile, filesToCleanUp: MutableList<File>): Boolean {
        var isMakeindexNeeded = false

        // To find out whether makeindex is needed is relatively expensive,
        // so we only do this the first time
        if (!runConfig.hasBeenRun) {
            val commandsInFileSet = mainFile.psiFile(environment.project)?.commandsInFileSet()?.mapNotNull { it.name } ?: emptyList()

            // Option 1 in http://mirrors.ctan.org/macros/latex/contrib/glossaries/glossariesbegin.pdf
            val usesTexForGlossaries = "\\" + LatexGenericRegularCommand.MAKENOIDXGLOSSARIES.commandWithSlash in commandsInFileSet

            if (usesTexForGlossaries) {
                runConfig.compileTwice = true
            }

            // If no index package is used, we assume we won't have to run makeindex
            val includedPackages = runConfig.mainFile
                ?.psiFile(runConfig.project)
                ?.includedPackages()
                ?: setOf()

            isMakeindexNeeded = includedPackages.intersect(PackageMagic.index + PackageMagic.glossary).isNotEmpty() && runConfig.compiler?.includesMakeindex == false && !usesTexForGlossaries

            // Some packages do handle makeindex themselves
            // Note that when you use imakeidx with the noautomatic option it won't, but we don't check for that
            if (includedPackages.contains(LatexPackage.IMAKEIDX) && !runConfig.usesAuxilOrOutDirectory()) {
                isMakeindexNeeded = false
            }
        }

        // Run makeindex when applicable
        if (runConfig.isFirstRunConfig && (runConfig.makeindexRunConfigs.isNotEmpty() || isMakeindexNeeded)) {
            handler.addProcessListener(RunMakeindexListener(runConfig, environment, filesToCleanUp))
        }

        return isMakeindexNeeded
    }

    private fun isLastCompile(isMakeindexNeeded: Boolean, handler: KillableProcessHandler): Boolean {
        // If there is no bibtex/makeindex involved and we don't need to compile twice, then this is the last compile
        if (runConfig.bibRunConfigs.isEmpty() && !isMakeindexNeeded && runConfig.externalToolRunConfigs.isEmpty()) {
            if (!runConfig.compileTwice) {
                runConfig.isLastRunConfig = true
            }

            // Schedule the second compile only if this is the first compile
            @Suppress("KotlinConstantConditions")
            if (!runConfig.isLastRunConfig && runConfig.compileTwice) {
                handler.addProcessListener(RunLatexListener(runConfig, environment))
                return false
            }
        }

        return true
    }

    private fun scheduleBibtexRunIfNeeded(handler: KillableProcessHandler) {
        runConfig.bibRunConfigs.forEachIndexed { index, bibSettings ->
            if (!runConfig.isFirstRunConfig) {
                return@forEachIndexed
            }

            // Only run latex after the last one
            if (index == runConfig.bibRunConfigs.size - 1) {
                handler.addProcessListener(RunBibtexListener(bibSettings, runConfig, environment, true))
            }
            else {
                handler.addProcessListener(RunBibtexListener(bibSettings, runConfig, environment, false))
            }
        }
    }

    private fun schedulePdfViewerIfNeeded(handler: KillableProcessHandler) {
        // Do not schedule to open the pdf viewer when this is not the last run config in the chain
        if (runConfig.isLastRunConfig) {
            addOpenViewerListener(handler, runConfig.allowFocusChange)
            handler.addProcessListener(AutoCompileDoneListener())
        }
    }

    private fun scheduleFileCleanup(filesToCleanUp: MutableList<File>, filesToCleanUpIfEmpty: Set<File>, handler: KillableProcessHandler) {
        if (runConfig.isLastRunConfig) {
            handler.addProcessListener(FileCleanupListener(filesToCleanUp, filesToCleanUpIfEmpty))
        }
    }

    /**
     * Add a certain process listener for opening the right pdf viewer depending on settings and OS.
     *
     * @param focusAllowed Whether focussing the pdf viewer is allowed. If not, it may happen forward search is not executed (in case the pdf viewer does not support forward search without changing focus).
     */
    private fun addOpenViewerListener(handler: ProcessHandler, focusAllowed: Boolean = true) {
        // First check if the user specified a custom viewer, if not then try other supported viewers
        if (!runConfig.viewerCommand.isNullOrEmpty()) {
            // Split user command on spaces, then replace {pdf} if needed
            val commandString = runConfig.viewerCommand!!

            // Split on spaces
            val commandList = commandString.split(" ").toMutableList()

            val containsPlaceholder = commandList.contains("{pdf}")

            if (containsPlaceholder) {
                // Replace placeholder
                for (i in 0 until commandList.size) {
                    if (commandList[i].contains("{pdf}")) {
                        commandList[i] = commandList[i].replace("{pdf}", runConfig.outputFilePath)
                    }
                }
            }
            else {
                // If no placeholder was used, assume the path is the final argument
                commandList += runConfig.outputFilePath
            }

            handler.addProcessListener(OpenCustomPdfViewerListener(commandList.toTypedArray(), runConfig = runConfig))
        }
        // Do nothing if the user selected that they do not want a viewer to open.
        else if (runConfig.pdfViewer == InternalPdfViewer.NONE) return
        // Sumatra does not support DVI
        else if (((runConfig.pdfViewer == InternalPdfViewer.SUMATRA && SumatraAvailabilityChecker.isSumatraAvailable)) && runConfig.outputFormat == LatexCompiler.Format.PDF) {
            // Open Sumatra after compilation & execute inverse search.
            handler.addProcessListener(SumatraForwardSearchListener(runConfig, environment))
        }
        else if (runConfig.pdfViewer is ExternalPdfViewer ||
            runConfig.pdfViewer in listOf(InternalPdfViewer.EVINCE, InternalPdfViewer.OKULAR, InternalPdfViewer.ZATHURA, InternalPdfViewer.SKIM)
        ) {
            ViewerForwardSearch(runConfig.pdfViewer ?: InternalPdfViewer.NONE).execute(handler, runConfig, environment, focusAllowed)
        }
        else if (SystemInfo.isMac) {
            // Open default system viewer, source: https://ss64.com/osx/open.html
            val commandList = arrayListOf("open", runConfig.outputFilePath)
            // Fail silently, otherwise users who have set up something themselves get an exception every time when this command fails
            handler.addProcessListener(OpenCustomPdfViewerListener(commandList.toTypedArray(), failSilently = true, runConfig = runConfig))
        }
        else if (SystemInfo.isLinux) {
            // Open default system viewer using xdg-open, since this is available in almost all desktop environments
            val commandList = arrayListOf("xdg-open", runConfig.outputFilePath)
            handler.addProcessListener(OpenCustomPdfViewerListener(commandList.toTypedArray(), failSilently = true, runConfig = runConfig))
        }
    }
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.ConfigurationType
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfigurationType
import nl.hannahsten.texifyidea.run.latex.externaltool.ExternalToolRunConfiguration
import nl.hannahsten.texifyidea.run.latex.externaltool.ExternalToolRunConfigurationType
import nl.hannahsten.texifyidea.run.makeindex.MakeindexRunConfiguration
import nl.hannahsten.texifyidea.run.makeindex.MakeindexRunConfigurationType

/**
 * @author Sten Wessel
 */
class LatexConfigurationFactory(type: ConfigurationType) : ConfigurationFactory(type) {

    companion object {

        private const val factoryName = "LaTeX configuration factory"
    }

    override fun createTemplateConfiguration(project: Project) = when (type) {
        is LatexRunConfigurationType -> LatexRunConfiguration(project, this, "LaTeX").apply {
            setDefaultCompiler()
            setDefaultPdfViewer()
            setDefaultOutputFormat()
            setSuggestedName()
            setDefaultDistribution(project)
        }
        is BibtexRunConfigurationType -> BibtexRunConfiguration(project, this, "BibTeX")
        is MakeindexRunConfigurationType -> MakeindexRunConfiguration(project, this, "Makeindex")
        is ExternalToolRunConfigurationType -> ExternalToolRunConfiguration(project, this, "External LaTeX tool")
        else -> throw IllegalArgumentException("No TeXiFy run configuration type, but ${type.id} was received instead.")
    }

    override fun getName() = factoryName

    override fun getId() = factoryName
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.settings.sdk.LatexSdk
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil

/**
 * Options for the run configuration.
 * See [LatexSdk].
 * This enum is still required, for example to support non-IntelliJ IDEs, but also to allow overriding the distribution type
 * when for example a different project SDK needs to be selected for a different language to work.
 */
enum class LatexDistributionType(val displayName: String) {

    TEXLIVE("TeX Live"),
    MIKTEX("MiKTeX"),
    WSL_TEXLIVE("TeX Live using WSL"),
    DOCKER_MIKTEX("Dockerized MiKTeX"),
    PROJECT_SDK("Use project SDK");

    private fun isMiktex() = this == MIKTEX || this == DOCKER_MIKTEX
    fun isMiktex(project: Project) = this == MIKTEX || this == DOCKER_MIKTEX || (this == PROJECT_SDK && LatexSdkUtil.getLatexProjectSdkType(project)?.getLatexDistributionType()?.isMiktex() == true)

    fun isAvailable(project: Project) = LatexSdkUtil.isAvailable(this, project)

    override fun toString() = displayName

    companion object {

        fun valueOfIgnoreCase(value: String?): LatexDistributionType {
            return values().firstOrNull { it.name.equals(value, true) } ?: TEXLIVE
        }
    }
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.execution.ExecutionException
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import nl.hannahsten.texifyidea.util.files.*
import java.io.File

/**
 * Output file as a virtual file, or a promise to provide a path that can be constructed when the run configuration is actually created.
 * This allows for custom output paths in the run configuration template.
 *
 * Supported placeholders:
 * - $contentRoot
 * - $mainFile
 *
 * @param variant: out or auxil
 */
class LatexOutputPath(private val variant: String, var contentRoot: VirtualFile?, var mainFile: VirtualFile?, private val project: Project) {

    companion object {

        const val projectDirString = "{projectDir}"
        const val mainFileString = "{mainFileParent}"
    }

    fun clone(): LatexOutputPath {
        return LatexOutputPath(variant, contentRoot, mainFile, project).apply { if (this@LatexOutputPath.pathString.isNotBlank()) this.pathString = this@LatexOutputPath.pathString }
    }

    // Acts as a sort of cache
    var virtualFile: VirtualFile? = null

    var pathString: String = "$projectDirString/$variant"

    /**
     * Get the output path based on the values of [virtualFile] and [pathString], create it if it does not exist.
     */
    fun getAndCreatePath(): VirtualFile? {
        // No auxil directory should be present/created when there's no MiKTeX around, assuming that TeX Live does not support this
        if (!LatexSdkUtil.isMiktexAvailable && variant == "auxil") {
            return null
        }

        // Just to be sure, avoid using jetbrains /bin path as output
        if (pathString.isBlank()) {
            pathString = "$projectDirString/$variant"
        }

        // Caching of the result
        return getPath().also {
            virtualFile = it
        }
    }

    private fun getPath(): VirtualFile? {
        // When we previously made the mistake of calling findRelativePath with an empty string, the output path will be set to thee /bin folder of IntelliJ. Fix that here, to be sure
        if (virtualFile?.path?.endsWith("/bin") == true) {
            virtualFile = null
        }

        if (virtualFile != null) {
            return virtualFile!!
        }
        else {
            val pathString = if (pathString.contains(projectDirString)) {
                if (contentRoot == null) return if (mainFile != null) mainFile?.parent else null
                pathString.replace(projectDirString, contentRoot?.path ?: return null)
            }
            else {
                if (mainFile == null) return null
                pathString.replace(mainFileString, mainFile?.parent?.path ?: return null)
            }
            val path = LocalFileSystem.getInstance().findFileByPath(pathString)
            if (path != null && path.isDirectory) {
                return path
            }
            else {
                // Try to create the path
                createOutputPath(pathString)?.let { return it }
            }
            // Path is invalid (perhaps the user provided an invalid path)
            Notification("LaTeX", "Invalid output path", "Output path $pathString of the run configuration could not be created, trying default path ${contentRoot?.path + "/" + variant}", NotificationType.WARNING).notify(project)

            // Create and return default path
            if (contentRoot != null) {
                val defaultPathString = contentRoot!!.path + "/" + variant
                createOutputPath(defaultPathString)?.let { return it }
            }

            if (contentRoot != null) {
                return contentRoot!!
            }

            return null
        }
    }

    private fun getDefaultOutputPath(): VirtualFile? {
        if (mainFile == null) return null
        var defaultOutputPath: VirtualFile? = null
        runReadAction {
            val moduleRoot = ProjectRootManager.getInstance(project).fileIndex.getContentRootForFile(mainFile!!)
            if (moduleRoot?.path != null) {
                defaultOutputPath = LocalFileSystem.getInstance().findFileByPath(moduleRoot.path + "/" + variant)
            }
        }
        return defaultOutputPath
    }

    /**
     * Whether the current output path is the default.
     */
    fun isDefault() = getDefaultOutputPath() == virtualFile

    /**
     * Creates the output directory to place all produced files.
     */
    private fun createOutputPath(outPath: String): VirtualFile? {
        val mainFile = mainFile ?: return null
        if (outPath.isBlank()) return null
        val fileIndex = ProjectRootManager.getInstance(project).fileIndex

        // Create output path for non-MiKTeX systems (MiKTeX creates it automatically)
        val module = runReadAction { fileIndex.getModuleForFile(mainFile, false) }
        if (File(outPath).mkdirs()) {
            module?.createExcludedDir(outPath)
            return LocalFileSystem.getInstance().refreshAndFindFileByPath(outPath)
        }
        return null
    }

    /**
     * Copy subdirectories of the source directory to the output directory for includes to work in non-MiKTeX systems.
     *
     * @return Set of directories that were created and did not exist already.
     */
    @Throws(ExecutionException::class)
    fun updateOutputSubDirs(): Set<File> {
        val includeRoot = mainFile?.parent
        val outPath = virtualFile?.path ?: return emptySet()

        // We cannot take the time to figure out the file set at this point, because it would take too long (up to 30 seconds for large projects).
        // However, we really want the most up-to-date directory structure from the source, because compilation would fail if we miss a subdirectory, so we cannot just do this once when generating the run configuration.
        // Therefore, creating a superset of the directories we need is probably best we can do (and maybe clean them up later)
        val files: Set<VirtualFile> = includeRoot?.allChildDirectories() ?: emptySet()
        val createdDirectories = mutableSetOf<File>()

        // Create output paths (see issue #70 on GitHub)
        files.asSequence()
            .mapNotNull {
                FileUtil.pathRelativeTo(includeRoot?.path ?: return@mapNotNull null, it.path)
            }
            .forEach {
                val file = File(outPath + it)
                if (file.mkdirs()) {
                    createdDirectories.add(file)
                }
            }

        return createdDirectories
    }
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.diagnostic.logging.LogConsoleManagerBase
import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor
import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.configuration.EnvironmentVariablesData
import com.intellij.execution.configurations.*
import com.intellij.execution.filters.RegexpFilter
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.InvalidDataException
import com.intellij.openapi.util.WriteExternalException
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.lang.magic.DefaultMagicKeys
import nl.hannahsten.texifyidea.lang.magic.allParentMagicComments
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfigurationType
import nl.hannahsten.texifyidea.run.compiler.BibliographyCompiler
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler.Format
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogTabComponent
import nl.hannahsten.texifyidea.run.latex.ui.LatexSettingsEditor
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer
import nl.hannahsten.texifyidea.run.pdfviewer.ExternalPdfViewers
import nl.hannahsten.texifyidea.run.pdfviewer.PdfViewer
import nl.hannahsten.texifyidea.run.sumatra.SumatraAvailabilityChecker
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import nl.hannahsten.texifyidea.util.parser.allCommands
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.files.findFile
import nl.hannahsten.texifyidea.util.files.findVirtualFileByAbsoluteOrRelativePath
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import nl.hannahsten.texifyidea.util.parser.hasBibliography
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.parser.usesBiber
import org.jdom.Element
import java.io.File
import java.util.*

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexRunConfiguration(
    project: Project,
    factory: ConfigurationFactory,
    name: String
) : RunConfigurationBase<LatexCommandLineState>(project, factory, name), LocatableConfiguration {

    companion object {

        private const val TEXIFY_PARENT = "texify"
        private const val COMPILER = "compiler"
        private const val COMPILER_PATH = "compiler-path"
        private const val SUMATRA_PATH = "sumatra-path"
        private const val PDF_VIEWER = "pdf-viewer"
        private const val VIEWER_COMMAND = "viewer-command"
        private const val COMPILER_ARGUMENTS = "compiler-arguments"
        private const val MAIN_FILE = "main-file"
        private const val OUTPUT_PATH = "output-path"
        private const val AUXIL_PATH = "auxil-path"
        private const val COMPILE_TWICE = "compile-twice"
        private const val OUTPUT_FORMAT = "output-format"
        private const val LATEX_DISTRIBUTION = "latex-distribution"
        private const val HAS_BEEN_RUN = "has-been-run"
        private const val BIB_RUN_CONFIG = "bib-run-config"
        private const val MAKEINDEX_RUN_CONFIG = "makeindex-run-config"

        // For backwards compatibility
        private const val AUX_DIR = "aux-dir"
        private const val OUT_DIR = "out-dir"
    }

    var compiler: LatexCompiler? = null
    var compilerPath: String? = null
    var sumatraPath: String? = null
    var enableSumatraPath: Boolean? = false
    var pdfViewer: PdfViewer? = null
    var viewerCommand: String? = null

    var compilerArguments: String? = null
        set(compilerArguments) {
            field = compilerArguments?.trim()
            if (field?.isEmpty() == true) {
                field = null
            }
        }

    var environmentVariables: EnvironmentVariablesData = EnvironmentVariablesData.DEFAULT

    var mainFile: VirtualFile? = null
        set(value) {
            field = value
            this.outputPath.mainFile = value
            this.outputPath.contentRoot = getMainFileContentRoot()
            this.auxilPath.mainFile = value
            this.auxilPath.contentRoot = getMainFileContentRoot()
        }

    // Save the psifile which can be used to check whether to create a bibliography based on which commands are in the psifile
    // This is not done when creating the template run configuration in order to delay the expensive bibtex check
    var psiFile: PsiFile? = null

    /** Path to the directory containing the output files. */
    var outputPath = LatexOutputPath("out", getMainFileContentRoot(), mainFile, project)

    /** Path to the directory containing the auxiliary files. */
    var auxilPath = LatexOutputPath("auxil", getMainFileContentRoot(), mainFile, project)

    var compileTwice = false
    var outputFormat: Format = Format.PDF

    /**
     * Use [getLatexDistributionType] to take the Project SDK into account.
     */
    internal var latexDistribution = LatexDistributionType.PROJECT_SDK

    /** Whether this run configuration is the last one in the chain of run configurations (e.g. latex, bibtex, latex, latex). */
    var isLastRunConfig = false
    var isFirstRunConfig = true

    // Whether the run configuration has already been run or not, since it has been created
    var hasBeenRun = false

    /** Whether the pdf viewer is allowed to claim focus after compilation. */
    var allowFocusChange = true

    private var bibRunConfigIds = mutableSetOf<String>()
    var bibRunConfigs: Set<RunnerAndConfigurationSettings>
        get() = bibRunConfigIds.mapNotNull {
            RunManagerImpl.getInstanceImpl(project).getConfigurationById(it)
        }.toSet()
        set(bibRunConfigs) {
            bibRunConfigIds = mutableSetOf()
            bibRunConfigs.forEach {
                bibRunConfigIds.add(it.uniqueID)
            }
        }

    private var makeindexRunConfigIds = mutableSetOf<String>()
    var makeindexRunConfigs: Set<RunnerAndConfigurationSettings>
        get() = makeindexRunConfigIds.mapNotNull {
            RunManagerImpl.getInstanceImpl(project).getConfigurationById(it)
        }.toSet()
        set(makeindexRunConfigs) {
            makeindexRunConfigIds = mutableSetOf()
            makeindexRunConfigs.forEach {
                makeindexRunConfigIds.add(it.uniqueID)
            }
        }

    private var externalToolRunConfigIds = mutableSetOf<String>()
    var externalToolRunConfigs: Set<RunnerAndConfigurationSettings>
        get() = externalToolRunConfigIds.mapNotNull {
            RunManagerImpl.getInstanceImpl(project).getConfigurationById(it)
        }.toSet()
        set(externalToolRunConfigs) {
            externalToolRunConfigIds = mutableSetOf()
            externalToolRunConfigs.forEach {
                externalToolRunConfigIds.add(it.uniqueID)
            }
        }

    // In order to propagate information about which files need to be cleaned up at the end between one run of the run config
    // (for example makeindex) and the last run, we save this information temporarily here while the run configuration is running.
    val filesToCleanUp = mutableListOf<File>()

    override fun getConfigurationEditor(): SettingsEditor<out RunConfiguration> {
        return LatexSettingsEditor(project)
    }

    override fun createAdditionalTabComponents(
        manager: AdditionalTabComponentManager,
        startedProcess: ProcessHandler?
    ) {
        super.createAdditionalTabComponents(manager, startedProcess)

        if (manager is LogConsoleManagerBase && startedProcess != null) {
            manager.addAdditionalTabComponent(LatexLogTabComponent(project, mainFile, startedProcess), "LaTeX-Log", AllIcons.Vcs.Changelist, false)
        }
    }

    @Throws(RuntimeConfigurationException::class)
    override fun checkConfiguration() {
        if (compiler == null) {
            throw RuntimeConfigurationError(
                "Run configuration is invalid: no compiler selected"
            )
        }
        if (mainFile == null) {
            throw RuntimeConfigurationError("Run configuration is invalid: no valid main LaTeX file selected")
        }

        // Updates the SumatraAvailabilityChecker with the path in sumatraPath after change.
        // Also checks if the path leads to a correct directory containing Sumatra.
        if (!SumatraAvailabilityChecker.isSumatraPathAvailable(sumatraPath).second && enableSumatraPath == true) {
            throw RuntimeConfigurationError("Run configuration is invalid: custom Sumatra path doesn't point to a valid directory")
        }
    }

    @Throws(ExecutionException::class)
    override fun getState(
        executor: Executor,
        environment: ExecutionEnvironment
    ): RunProfileState {
        val filter = RegexpFilter(
            environment.project,
            "^\$FILE_PATH$:\$LINE$"
        )

        val state = LatexCommandLineState(
            environment,
            this
        )
        state.addConsoleFilters(filter)
        return state
    }

    @Throws(InvalidDataException::class)
    override fun readExternal(element: Element) {
        super<RunConfigurationBase>.readExternal(element)

        val parent = element.getChild(TEXIFY_PARENT) ?: return

        // Read compiler.
        val compilerName = parent.getChildText(COMPILER)
        try {
            this.compiler = LatexCompiler.valueOf(compilerName)
        }
        catch (e: IllegalArgumentException) {
            this.compiler = null
        }

        // Read compiler custom path.
        val compilerPathRead = parent.getChildText(COMPILER_PATH)
        this.compilerPath = if (compilerPathRead.isNullOrEmpty()) null else compilerPathRead

        // Read SumatraPDF custom path
        val sumatraPathRead = parent.getChildText(SUMATRA_PATH)
        this.sumatraPath = if (sumatraPathRead.isNullOrEmpty()) null else sumatraPathRead
        // Updates the SumatraAvailabilityChecker at startup
        SumatraAvailabilityChecker.isSumatraPathAvailable(this.sumatraPath)

        // Read pdf viewer.
        val viewerName = parent.getChildText(PDF_VIEWER)
        try {
            this.pdfViewer = ExternalPdfViewers.getExternalPdfViewers().firstOrNull { it.name == viewerName }
                ?: InternalPdfViewer.values().firstOrNull { it.name == viewerName && it.isAvailable() }
                ?: InternalPdfViewer.NONE
        }
        catch (e: IllegalArgumentException) {
            // Try to recover from old settings (when the pdf viewer was set in the TeXiFy settings instead of the run config).
            this.pdfViewer = TexifySettings.getInstance().pdfViewer
        }

        // Read custom pdf viewer command
        val viewerCommandRead = parent.getChildText(VIEWER_COMMAND)
        this.viewerCommand = if (viewerCommandRead.isNullOrEmpty()) null else viewerCommandRead

        // Read compiler arguments.
        val compilerArgumentsRead = parent.getChildText(COMPILER_ARGUMENTS)
        compilerArguments = if (compilerArgumentsRead.isNullOrEmpty()) null else compilerArgumentsRead

        // Read environment variables
        environmentVariables = EnvironmentVariablesData.readExternal(parent)

        // Read main file.
        val filePath = parent.getChildText(MAIN_FILE)
        setMainFile(filePath)

        // Read output path
        val outputPathString = parent.getChildText(OUTPUT_PATH)
        if (outputPathString != null) {
            if (outputPathString.endsWith("/bin")) {
                this.outputPath = LatexOutputPath("out", getMainFileContentRoot(), mainFile, project)
            }
            else {
                this.outputPath = LatexOutputPath("out", getMainFileContentRoot(), mainFile, project)
                this.outputPath.pathString = outputPathString
            }
        }

        // Read auxil path
        val auxilPathString = parent.getChildText(AUXIL_PATH)
        if (auxilPathString != null) {
            this.auxilPath = LatexOutputPath("auxil", getMainFileContentRoot(), mainFile, project)
            this.auxilPath.pathString = auxilPathString
        }

        // Backwards compatibility
        runReadAction {
            val auxDirBoolean = parent.getChildText(AUX_DIR)
            if (auxDirBoolean != null && this.auxilPath.virtualFile == null && this.mainFile != null) {
                // If there is no auxil path yet but this option still exists,
                // guess the output path in the same way as it was previously done
                val usesAuxDir = java.lang.Boolean.parseBoolean(auxDirBoolean)
                val moduleRoot = ProjectRootManager.getInstance(project).fileIndex.getContentRootForFile(this.mainFile!!)
                val path = if (usesAuxDir) moduleRoot?.path + "/auxil" else this.mainFile!!.parent.path
                this.auxilPath.virtualFile = LocalFileSystem.getInstance().findFileByPath(path)
            }
            val outDirBoolean = parent.getChildText(OUT_DIR)
            if (outDirBoolean != null && this.outputPath.virtualFile == null && this.mainFile != null) {
                val usesOutDir = java.lang.Boolean.parseBoolean(outDirBoolean)
                val moduleRoot = ProjectRootManager.getInstance(project).fileIndex.getContentRootForFile(this.mainFile!!)
                val path = if (usesOutDir) moduleRoot?.path + "/out" else this.mainFile!!.parent.path
                this.outputPath.virtualFile = LocalFileSystem.getInstance().findFileByPath(path)
            }
        }

        // Read whether to compile twice
        val compileTwiceBoolean = parent.getChildText(COMPILE_TWICE)
        if (compileTwiceBoolean == null) {
            this.compileTwice = false
        }
        else {
            this.compileTwice = compileTwiceBoolean.toBoolean()
        }

        // Read output format.
        this.outputFormat = Format.byNameIgnoreCase(parent.getChildText(OUTPUT_FORMAT))

        // Read LatexDistribution
        this.latexDistribution = LatexDistributionType.valueOfIgnoreCase(parent.getChildText(LATEX_DISTRIBUTION))

        // Read whether the run config has been run
        this.hasBeenRun = parent.getChildText(HAS_BEEN_RUN)?.toBoolean() ?: false

        // Read bibliography run configurations, which is a list of ids
        val bibRunConfigElt = parent.getChildText(BIB_RUN_CONFIG)
        // Assume the list is of the form [id 1,id 2]
        this.bibRunConfigIds = bibRunConfigElt?.drop(1)?.dropLast(1)?.split(", ")?.toMutableSet() ?: mutableSetOf()

        // Read makeindex run configurations
        val makeindexRunConfigElt = parent.getChildText(MAKEINDEX_RUN_CONFIG)
        if (makeindexRunConfigElt != null) {
            this.makeindexRunConfigIds = makeindexRunConfigElt.drop(1).dropLast(1).split(", ").toMutableSet()
        }
    }

    @Throws(WriteExternalException::class)
    override fun writeExternal(element: Element) {
        super<RunConfigurationBase>.writeExternal(element)

        var parent: Element? = element.getChild(TEXIFY_PARENT)

        // Create a new parent when there is no parent present.
        if (parent == null) {
            parent = Element(TEXIFY_PARENT)
            element.addContent(parent)
        }
        else {
            // Otherwise overwrite (remove + write).
            parent.removeContent()
        }

        parent.addContent(Element(COMPILER).also { it.text = compiler?.name ?: "" })
        parent.addContent(Element(COMPILER_PATH).also { it.text = compilerPath ?: "" })
        parent.addContent(Element(SUMATRA_PATH).also { it.text = sumatraPath ?: "" })
        parent.addContent(Element(PDF_VIEWER).also { it.text = pdfViewer?.name ?: "" })
        parent.addContent(Element(VIEWER_COMMAND).also { it.text = viewerCommand ?: "" })
        parent.addContent(Element(COMPILER_ARGUMENTS).also { it.text = this.compilerArguments ?: "" })
        this.environmentVariables.writeExternal(parent)
        parent.addContent(Element(MAIN_FILE).also { it.text = mainFile?.path ?: "" })
        parent.addContent(Element(OUTPUT_PATH).also { it.text = outputPath.virtualFile?.path ?: outputPath.pathString })
        parent.addContent(Element(AUXIL_PATH).also { it.text = auxilPath.virtualFile?.path ?: auxilPath.pathString })
        parent.addContent(Element(COMPILE_TWICE).also { it.text = compileTwice.toString() })
        parent.addContent(Element(OUTPUT_FORMAT).also { it.text = outputFormat.name })
        parent.addContent(Element(LATEX_DISTRIBUTION).also { it.text = latexDistribution.name })
        parent.addContent(Element(HAS_BEEN_RUN).also { it.text = hasBeenRun.toString() })
        parent.addContent(Element(BIB_RUN_CONFIG).also { it.text = bibRunConfigIds.toString() })
        parent.addContent(Element(MAKEINDEX_RUN_CONFIG).also { it.text = makeindexRunConfigIds.toString() })
    }

    /**
     * Create a new bib run config and add it to the set.
     */
    private fun addBibRunConfig(defaultCompiler: BibliographyCompiler, mainFile: VirtualFile?, compilerArguments: String? = null) {
        val runManager = RunManagerImpl.getInstanceImpl(project)

        val bibSettings = runManager.createConfiguration(
            "",
            LatexConfigurationFactory(BibtexRunConfigurationType())
        )

        val bibtexRunConfiguration = bibSettings.configuration as BibtexRunConfiguration

        bibtexRunConfiguration.compiler = defaultCompiler
        if (compilerArguments != null) bibtexRunConfiguration.compilerArguments = compilerArguments
        bibtexRunConfiguration.mainFile = mainFile
        bibtexRunConfiguration.setSuggestedName()

        // On non-MiKTeX systems, add bibinputs for bibtex to work
        if (!latexDistribution.isMiktex(project)) {
            // Only if default, because the user could have changed it after creating the run config but before running
            if (mainFile != null && outputPath.virtualFile != mainFile.parent) {
                // As seen in issue 2165, appending a colon (like with TEXINPUTS) may not work on Windows,
                // however it may be necessary on Mac/Linux as seen in #2249.
                bibtexRunConfiguration.environmentVariables = bibtexRunConfiguration.environmentVariables.with(mapOf("BIBINPUTS" to mainFile.parent.path, "BSTINPUTS" to mainFile.parent.path + File.pathSeparator))
            }
        }

        runManager.addConfiguration(bibSettings)

        bibRunConfigs = bibRunConfigs + setOf(bibSettings)
    }

    /**
     * Generate a Bibtex run configuration, after trying to guess whether the user wants to use bibtex or biber as compiler.
     */
    internal fun generateBibRunConfig() {
        // Get a pair of Bib compiler and compiler arguments.
        val compilerFromMagicComment: Pair<BibliographyCompiler, String>? by lazy {
            val runCommand = psiFile?.allParentMagicComments()
                ?.value(DefaultMagicKeys.BIBTEXCOMPILER) ?: return@lazy null
            val compilerString = if (runCommand.contains(' ')) {
                runCommand.let { it.subSequence(0, it.indexOf(' ')) }.trim()
                    .toString()
            }
            else runCommand
            val compiler = BibliographyCompiler.valueOf(compilerString.uppercase(Locale.getDefault()))
            val compilerArguments = runCommand.removePrefix(compilerString)
                .trim()
            Pair(compiler, compilerArguments)
        }

        val defaultCompiler = when {
            compilerFromMagicComment != null -> compilerFromMagicComment!!.first
            psiFile?.hasBibliography() == true -> BibliographyCompiler.BIBTEX
            psiFile?.usesBiber() == true -> BibliographyCompiler.BIBER
            else -> return // Do not auto-generate a bib run config when we can't detect bibtex
        }

        // When chapterbib is used, every chapter has its own bibliography and needs its own run config
        val usesChapterbib = psiFile?.includedPackages()?.contains(LatexPackage.CHAPTERBIB) == true

        if (!usesChapterbib) {
            addBibRunConfig(defaultCompiler, mainFile, compilerFromMagicComment?.second)
        }
        else if (psiFile != null) {
            val allBibliographyCommands = psiFile!!.commandsInFileSet().filter { it.name == LatexGenericRegularCommand.BIBLIOGRAPHY.cmd }

            // We know that there can only be one bibliography per top-level \include,
            // however not all of them may contain a bibliography, and the ones
            // that do have one can have it in any included file
            psiFile!!.allCommands()
                .filter { it.name == LatexGenericRegularCommand.INCLUDE.cmd }
                .flatMap { command -> command.getRequiredParameters() }
                .forEach { filename ->
                    // Find all the files of this chapter, then check if any of the bibliography commands appears in a file in this chapter
                    val chapterMainFile = psiFile!!.findFile(filename)
                        ?: return@forEach

                    val chapterFiles = chapterMainFile.referencedFileSet()
                        .toMutableSet().apply { add(chapterMainFile) }

                    val chapterHasBibliography = allBibliographyCommands.any { it.containingFile in chapterFiles }

                    if (chapterHasBibliography) {
                        addBibRunConfig(defaultCompiler, chapterMainFile.virtualFile, compilerFromMagicComment?.second)
                    }
                }
        }
    }

    /**
     * All run configs in the chain except the LaTeX ones.
     */
    fun getAllAuxiliaryRunConfigs(): Set<RunnerAndConfigurationSettings> {
        return bibRunConfigs + makeindexRunConfigs + externalToolRunConfigs
    }

    /**
     * Looks up the corresponding [VirtualFile] and sets [LatexRunConfiguration.mainFile].
     */
    fun setMainFile(mainFilePath: String) {
        val fileSystem = LocalFileSystem.getInstance()
        // Check if the file is valid and exists
        val mainFile = fileSystem.findFileByPath(mainFilePath)
        if (mainFile?.extension == "tex") {
            this.mainFile = mainFile
            return
        }
        else {
            // Maybe it is a relative path
            ProjectRootManager.getInstance(project).contentRoots.forEach {
                val file = it.findFileByRelativePath(mainFilePath)
                if (file?.extension == "tex") {
                    this.mainFile = file
                    return
                }
            }
        }

        this.mainFile = null
    }

    fun setDefaultCompiler() {
        compiler = LatexCompiler.PDFLATEX
    }

    fun setDefaultPdfViewer() {
        pdfViewer = InternalPdfViewer.firstAvailable()
    }

    fun setDefaultOutputFormat() {
        outputFormat = Format.PDF
    }

    fun setDefaultDistribution(project: Project) {
        latexDistribution = LatexSdkUtil.getDefaultLatexDistributionType(project)
    }

    /**
     * Get LaTeX distribution type, when 'Use project SDK' is selected map it to a [LatexDistributionType].
     */
    fun getLatexDistributionType(): LatexDistributionType {
        return if (latexDistribution != LatexDistributionType.PROJECT_SDK) {
            latexDistribution
        }
        else {
            LatexSdkUtil.getLatexProjectSdkType(project)?.getLatexDistributionType() ?: LatexDistributionType.TEXLIVE
        }
    }

    /**
     * Find the directory where auxiliary files will be placed, depending on the run config settings.
     *
     * @return The auxil folder when MiKTeX used, or else the out folder when used.
     */
    fun getAuxilDirectory(): VirtualFile? {
        return if (latexDistribution.isMiktex(project)) {
            auxilPath.getAndCreatePath()
        }
        else {
            outputPath.getAndCreatePath()
        }
    }

    fun setSuggestedName() {
        setName(suggestedName())
    }

    override fun isGeneratedName(): Boolean {
        if (mainFile == null) {
            return false
        }

        val name = mainFile!!.nameWithoutExtension
        return name == getName()
    }

    // Path to output file (e.g. pdf)
    override fun getOutputFilePath(): String {
        val outputDir = outputPath.getAndCreatePath()
        return "${outputDir?.path}/" + mainFile!!
            .nameWithoutExtension + "." + if (outputFormat == Format.DEFAULT) "pdf"
        else outputFormat.toString()
            .lowercase(Locale.getDefault())
    }

    /**
     * Set [outputPath]
     */
    override fun setFileOutputPath(fileOutputPath: String) {
        if (fileOutputPath.isBlank()) return
        this.outputPath.virtualFile = findVirtualFileByAbsoluteOrRelativePath(fileOutputPath, project)
        // If not possible to resolve directly, we might resolve it later
        if (this.outputPath.virtualFile == null) {
            this.outputPath.pathString = fileOutputPath
        }
    }

    /**
     * Get the content root of the main file.
     */
    fun getMainFileContentRoot(): VirtualFile? {
        if (mainFile == null) return null
        if (!project.isInitialized) return null
        return runReadAction {
            return@runReadAction ProjectRootManager.getInstance(project).fileIndex.getContentRootForFile(mainFile!!)
        }
    }

    /**
     * Set [auxilPath]
     */
    fun setFileAuxilPath(fileAuxilPath: String) {
        if (fileAuxilPath.isBlank()) return
        this.auxilPath.virtualFile = findVirtualFileByAbsoluteOrRelativePath(fileAuxilPath, project)
        // If not possible to resolve directly, we might resolve it later
        if (this.auxilPath.virtualFile == null) {
            this.auxilPath.pathString = fileAuxilPath
        }
    }

    /**
     * Whether an auxil or out directory is used, i.e. whether not both are set to the directory of the main file
     */
    fun usesAuxilOrOutDirectory(): Boolean {
        val usesAuxilDir = auxilPath.getAndCreatePath() != mainFile?.parent
        val usesOutDir = outputPath.getAndCreatePath() != mainFile?.parent

        return usesAuxilDir || usesOutDir
    }

    override fun suggestedName(): String? {
        return if (mainFile == null) {
            null
        }
        else {
            mainFile!!.nameWithoutExtension
        }
    }

    override fun toString(): String {
        return "LatexRunConfiguration{" + "compiler=" + compiler +
            ", compilerPath=" + compilerPath +
            ", sumatraPath=" + sumatraPath +
            ", mainFile=" + mainFile +
            ", outputFormat=" + outputFormat +
            '}'.toString()
    }

    // Explicitly deep clone references, otherwise a copied run config has references to the original objects
    override fun clone(): RunConfiguration {
        return super.clone().also {
            val runConfiguration = it as? LatexRunConfiguration ?: return@also
            runConfiguration.outputPath = this.outputPath.clone()
            runConfiguration.auxilPath = this.auxilPath.clone()
        }
    }
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.execution.actions.ConfigurationContext
import com.intellij.execution.actions.LazyRunConfigurationProducer
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.lang.magic.DefaultMagicKeys
import nl.hannahsten.texifyidea.lang.magic.allParentMagicComments
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler

/**
 * @author Hannah Schellekens
 */
class LatexRunConfigurationProducer : LazyRunConfigurationProducer<LatexRunConfiguration>() {

    override fun getConfigurationFactory(): ConfigurationFactory {
        return LatexConfigurationFactory(LatexRunConfigurationType.instance)
    }

    override fun setupConfigurationFromContext(
        runConfiguration: LatexRunConfiguration,
        context: ConfigurationContext,
        sourceElement: Ref<PsiElement>
    ): Boolean {
        val location = context.location ?: return false
        val container = location.psiElement.containingFile ?: return false
        val mainFile = container.virtualFile ?: return false

        // Only activate on .tex files.
        val extension = mainFile.extension
        val texTension = LatexFileType.defaultExtension
        if (extension == null || !extension.equals(texTension, ignoreCase = true)) {
            return false
        }

        // Change the main file as given by the template run configuration to the current file
        runConfiguration.mainFile = mainFile
        runConfiguration.psiFile = container
        runConfiguration.setSuggestedName()
        // Avoid changing the outputPath of the template run config (which is a shallow clone)
        runConfiguration.outputPath = runConfiguration.outputPath.clone()
        runConfiguration.auxilPath = runConfiguration.auxilPath.clone()

        val runCommand = container.allParentMagicComments().value(DefaultMagicKeys.COMPILER)
        val runProgram = container.allParentMagicComments().value(DefaultMagicKeys.PROGRAM)
        val command = runCommand ?: runProgram ?: return true
        val compiler = if (command.contains(' ')) {
            command.let { it.subSequence(0, it.indexOf(' ')) }.trim().toString()
        }
        else command
        runConfiguration.compiler = LatexCompiler.byExecutableName(compiler)
        runConfiguration.compilerArguments = command.removePrefix(compiler).trim()
        return true
    }

    override fun isConfigurationFromContext(
        runConfiguration: LatexRunConfiguration,
        context: ConfigurationContext
    ): Boolean {
        val mainFile = runConfiguration.mainFile
        val psiFile = context.dataContext.getData(PlatformDataKeys.PSI_FILE) ?: return false
        val currentFile = psiFile.virtualFile ?: return false
        return mainFile?.path == currentFile.path
    }
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.ConfigurationType
import com.intellij.execution.configurations.ConfigurationTypeUtil
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexRunConfigurationType : ConfigurationType {

    companion object {

        val instance: LatexRunConfigurationType
            get() = ConfigurationTypeUtil.findConfigurationType(LatexRunConfigurationType::class.java)
    }

    override fun getDisplayName() = "LaTeX"

    override fun getConfigurationTypeDescription() = "Build a LaTeX file"

    override fun getIcon() = TexifyIcons.BUILD

    override fun getId() = "LATEX_RUN_CONFIGURATION"

    override fun getConfigurationFactories(): Array<ConfigurationFactory> = arrayOf(LatexConfigurationFactory(this))
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.execution.ExecutionException
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.parser.toStringMap
import nl.hannahsten.texifyidea.run.compiler.MakeindexProgram
import nl.hannahsten.texifyidea.util.SystemEnvironment
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.PackageMagic

/**
 * Try to find out which index program the user wants to use, based on the given options.
 * This can be multiple, if the user for example uses an index and a glossary.
 */
fun getDefaultMakeindexPrograms(mainFile: VirtualFile?, project: Project): Set<MakeindexProgram> {
    val indexPackageOptions = getIndexPackageOptions(mainFile, project)
    val makeindexOptions = getMakeindexOptions(mainFile, project)

    val usedPackages = runReadAction {
        mainFile?.psiFile(project)?.includedPackages() ?: emptySet()
    }

    val indexPrograms = mutableSetOf<MakeindexProgram>()

    if (usedPackages.intersect(PackageMagic.index).isNotEmpty()) {
        val makeindexProgram = if (indexPackageOptions.contains("xindy")) MakeindexProgram.XINDY else MakeindexProgram.MAKEINDEX
        indexPrograms.add(makeindexProgram)
    }

    if (LatexPackage.GLOSSARIES in usedPackages) {
        val glossaryProgram = if (SystemEnvironment.isAvailable("perl")) {
            MakeindexProgram.MAKEGLOSSARIES
        }
        else {
            MakeindexProgram.MAKEGLOSSARIESLITE
        }
        indexPrograms.add(glossaryProgram)
    }
    else if (LatexPackage.GLOSSARIESEXTRA in usedPackages && "record" in indexPackageOptions) {
        indexPrograms.add(MakeindexProgram.BIB2GLS)
    }

    // Possible extra settings to override the indexProgram, see the imakeidx docs
    if (makeindexOptions.contains("makeindex")) {
        indexPrograms.add(MakeindexProgram.MAKEINDEX)
    }
    else if (makeindexOptions.contains("xindy") || makeindexOptions.contains("texindy")) {
        indexPrograms.add(MakeindexProgram.XINDY)
    }
    else if (makeindexOptions.contains("truexindy")) {
        indexPrograms.add(MakeindexProgram.TRUEXINDY)
    }

    return indexPrograms
}

/**
 * Get package options for included index packages.
 */
private fun getIndexPackageOptions(mainFile: VirtualFile?, project: Project): List<String> {
    return runReadAction {
        // Find index package options
        val mainPsiFile = mainFile?.psiFile(project) ?: throw ExecutionException("Main file not found")
        LatexCommandsIndex.getItemsInFileSet(mainPsiFile)
            .filter { it.commandToken.text in CommandMagic.packageInclusionCommands }
            .filter { command -> command.getRequiredParameters().any { it in PackageMagic.index.map { pkg -> pkg.name } || it in PackageMagic.glossary.map { pkg -> pkg.name } } }
            .flatMap { it.getOptionalParameterMap().toStringMap().keys }
    }
}

/**
 * Get optional parameters of the \makeindex command. If an option key does not have a value it will map to the empty string.
 */
fun getMakeindexOptions(mainFile: VirtualFile?, project: Project): Map<String, String> {
    return runReadAction {
        val mainPsiFile = mainFile?.psiFile(project)
        if (mainPsiFile == null) {
            Notification("LaTeX", "Could not find main file ${mainFile?.path}", "Please make sure the main file exists.", NotificationType.ERROR).notify(project)
            return@runReadAction mapOf<String, String>()
        }

        val makeindexOptions = HashMap<String, String>()
        LatexCommandsIndex.getItemsInFileSet(mainPsiFile)
            .filter { it.commandToken.text == "\\makeindex" }
            .forEach {
                makeindexOptions.putAll(it.getOptionalParameterMap().toStringMap())
            }
        makeindexOptions
    }
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.execution.impl.RunConfigurationBeforeRunProvider
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.util.Key

/**
 * Compile LaTeX (again) once a process is terminated and initiate forward search.
 *
 * @author Thomas Schouten
 */
class RunLatexListener(
    private val runConfig: LatexRunConfiguration,
    private val environment: ExecutionEnvironment
) : ProcessListener {

    override fun processTerminated(event: ProcessEvent) {
        if (event.exitCode != 0) {
            return
        }

        val latexSettings = RunManagerImpl.getInstanceImpl(environment.project).getSettings(runConfig)
            ?: return
        runConfig.isLastRunConfig = true
        RunConfigurationBeforeRunProvider.doExecuteTask(environment, latexSettings, null)
        runConfig.isLastRunConfig = false
    }

    override fun onTextAvailable(p0: ProcessEvent, p1: Key<*>) {
        // Do nothing.
    }

    override fun processWillTerminate(p0: ProcessEvent, p1: Boolean) {
        // Do nothing.
    }

    override fun startNotified(p0: ProcessEvent) {
        // Do nothing.
    }
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer

import com.intellij.openapi.util.SystemInfo
import nl.hannahsten.texifyidea.run.linuxpdfviewer.evince.EvinceConversation
import nl.hannahsten.texifyidea.run.linuxpdfviewer.okular.OkularConversation
import nl.hannahsten.texifyidea.run.linuxpdfviewer.skim.SkimConversation
import nl.hannahsten.texifyidea.run.linuxpdfviewer.zathura.ZathuraConversation
import nl.hannahsten.texifyidea.run.pdfviewer.PdfViewer
import nl.hannahsten.texifyidea.run.sumatra.SumatraAvailabilityChecker
import nl.hannahsten.texifyidea.run.sumatra.SumatraConversation
import nl.hannahsten.texifyidea.util.runCommand

/**
 * List of supported PDF viewers on Linux.
 *
 * These pdf viewers have their support built-in in TeXiFy.
 * @param viewerCommand The command to call the viewer from the command line.
 * @param conversation The conversation class needed/used to talk to this viewer.
 */
enum class InternalPdfViewer(
    private val viewerCommand: String,
    override val displayName: String,
    val conversation: ViewerConversation?
) : PdfViewer {

    EVINCE("evince", "Evince", EvinceConversation),
    OKULAR("okular", "Okular", OkularConversation),
    ZATHURA("zathura", "Zathura", ZathuraConversation),
    SKIM("skim", "Skim", SkimConversation),
    SUMATRA("sumatra", "Sumatra", SumatraConversation),
    NONE("", "No PDF viewer", null);

    /**
     * Check if the viewer is installed and available from the path.
     */
    override fun isAvailable(): Boolean {
        // Using no PDF viewer should always be an option.
        return if (this == NONE) {
            true
        }
        else if (SystemInfo.isWindows && this == SUMATRA) {
            SumatraAvailabilityChecker.isSumatraAvailable
        }
        // Only support Evince and Okular on Linux, although they can be installed on other systems like Mac.
        else if (SystemInfo.isLinux) {
            // Find out whether the pdf viewer is installed and in PATH, otherwise we can't use it.
            val output = "which ${this.viewerCommand}".runCommand()
            output?.contains("/${this.viewerCommand}") ?: false
        }
        else if (SystemInfo.isMac) {
            // Check if Skim is installed in applications, otherwise we can't use it.
            // Open -Ra returns an error message if application was not found, else empty string
            val output = "open -Ra ${this.viewerCommand}".runCommand()
            output?.isEmpty() ?: false
        }
        else {
            false
        }
    }

    override fun toString(): String = displayName

    companion object {

        fun availableSubset(): List<InternalPdfViewer> = values().filter { it.isAvailable() }

        fun firstAvailable(): InternalPdfViewer = availableSubset().first()
    }
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer

import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.action.ForwardSearchAction
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.run.pdfviewer.ExternalPdfViewer
import nl.hannahsten.texifyidea.run.pdfviewer.PdfViewer
import org.jetbrains.concurrency.runAsync

/**
 * Execute a forward search with the selected viewer after the compilation is done.
 */
class OpenViewerListener(
    private val viewer: PdfViewer,
    val runConfig: LatexRunConfiguration,
    private val sourceFilePath: String,
    val line: Int,
    val project: Project,
    val focusAllowed: Boolean = true
) :
    ProcessListener {

    override fun processTerminated(event: ProcessEvent) {
        if (event.exitCode == 0) {
            runAsync {
                try {
                    when (viewer) {
                        is InternalPdfViewer -> viewer.conversation?.forwardSearch(pdfPath = runConfig.outputFilePath, sourceFilePath = sourceFilePath, line = line, project = project, focusAllowed = focusAllowed)
                        is ExternalPdfViewer -> viewer.forwardSearch(pdfPath = runConfig.outputFilePath, sourceFilePath = sourceFilePath, line = line, project = project, focusAllowed = focusAllowed)
                        else -> {}
                    }
                    // Set this viewer as viewer to forward search to in the future.
                    (ActionManager.getInstance().getAction("texify.ForwardSearch") as? ForwardSearchAction)?.viewer = viewer
                }
                catch (ignored: TeXception) {
                }
            }
        }

        // Reset to default
        runConfig.allowFocusChange = true
    }

    override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
        // Do nothing.
    }

    override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) {
        // Do nothing.
    }

    override fun startNotified(event: ProcessEvent) {
        // Do nothing.
    }
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer

import com.intellij.openapi.project.Project

abstract class ViewerConversation {

    abstract fun forwardSearch(pdfPath: String?, sourceFilePath: String, line: Int, project: Project, focusAllowed: Boolean)
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer

import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.run.pdfviewer.PdfViewer
import nl.hannahsten.texifyidea.util.caretOffset
import nl.hannahsten.texifyidea.util.currentTextEditor
import nl.hannahsten.texifyidea.util.files.psiFile

/**
 * Provides forward search for the available pdf viewers.
 */
class ViewerForwardSearch(private val viewer: PdfViewer) {

    /**
     * Execute forward search when the process is done.
     *
     * In the case that no tex file is open, forward search from the first line of the main file that is selected in the
     * run config.
     */
    fun execute(handler: ProcessHandler, runConfig: LatexRunConfiguration, environment: ExecutionEnvironment, focusAllowed: Boolean = true) {
        // We have to find the file and line number before scheduling the forward search.
        val editor = environment.project.currentTextEditor()?.editor

        // Get the line number in the currently open file
        val line = editor?.document?.getLineNumber(editor.caretOffset())?.plus(1) ?: 0

        // Get the currently open file to use for forward search.
        val currentPsiFile = editor?.document?.psiFile(environment.project)
            // Get the main file from the run configuration as a fallback.
            ?: runConfig.mainFile?.psiFile(environment.project)
            ?: return

        // Set the OpenViewerListener to execute when the compilation is done.
        handler.addProcessListener(OpenViewerListener(viewer, runConfig, currentPsiFile.virtualFile.path, line, environment.project, focusAllowed))
    }
}

package nl.hannahsten.texifyidea.run.makeindex

import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.execution.ParametersListUtil
import nl.hannahsten.texifyidea.run.compiler.MakeindexProgram
import nl.hannahsten.texifyidea.util.appendExtension

/**
 * Run makeindex.
 */
class MakeindexCommandLineState(
    environment: ExecutionEnvironment,
    private val mainFile: VirtualFile?,
    private val workingDirectory: VirtualFile?,
    private val makeindexOptions: Map<String, String>,
    private val indexProgram: MakeindexProgram,
    private val commandLineArguments: String?,
) : CommandLineState(environment) {

    @Throws(ExecutionException::class)
    override fun startProcess(): ProcessHandler {
        if (mainFile == null) {
            throw ExecutionException("Main file to compile is not found or missing.")
        }

        val indexBasename = makeindexOptions.getOrDefault("name", mainFile.nameWithoutExtension)

        // texindy requires the file extension
        val indexFilename = if (indexProgram != MakeindexProgram.XINDY) indexBasename else indexBasename.appendExtension("idx")

        val command = ParametersListUtil.parse(commandLineArguments ?: "").apply {
            add(0, indexProgram.executableName)
            add(indexFilename)
        }
        val commandLine = GeneralCommandLine(command).withWorkDirectory(workingDirectory?.path)

        val handler: ProcessHandler = KillableProcessHandler(commandLine)

        // Reports exit code to run output window when command is terminated
        ProcessTerminatedListener.attach(handler, environment.project)

        return handler
    }
}

package nl.hannahsten.texifyidea.run.makeindex

import com.intellij.execution.Executor
import com.intellij.execution.configurations.*
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.compiler.MakeindexProgram
import nl.hannahsten.texifyidea.run.latex.getMakeindexOptions
import org.jdom.Element
import java.util.*

/**
 * Run configuration for running the makeindex tool.
 */
class MakeindexRunConfiguration(
    project: Project,
    factory: ConfigurationFactory,
    name: String
) : RunConfigurationBase<MakeindexCommandLineState>(project, factory, name), LocatableConfiguration {

    companion object {

        private const val PARENT_ELEMENT = "texify-makeindex"
        private const val PROGRAM = "program"
        private const val MAIN_FILE = "main-file"
        private const val COMMAND_LINE_ARGS = "command-line-args"
        private const val WORK_DIR = "work-dir"
    }

    // The following variables are saved in the MakeindexSettingsEditor
    var makeindexProgram: MakeindexProgram = MakeindexProgram.MAKEINDEX
    var mainFile: VirtualFile? = null
    var commandLineArguments: String? = null
    var workingDirectory: VirtualFile? = null

    override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState {
        val makeindexOptions = getMakeindexOptions(mainFile, project)
        return MakeindexCommandLineState(environment, mainFile, workingDirectory, makeindexOptions, makeindexProgram, commandLineArguments)
    }

    override fun getConfigurationEditor(): SettingsEditor<out RunConfiguration> = MakeindexSettingsEditor(project)

    override fun readExternal(element: Element) {
        super<LocatableConfiguration>.readExternal(element)

        val parent = element.getChild(PARENT_ELEMENT)

        try {
            val programText = parent.getChildText(PROGRAM)
            if (!programText.isNullOrEmpty()) {
                makeindexProgram = MakeindexProgram.valueOf(programText)
            }
        }
        catch (ignored: NullPointerException) {
        }

        val mainFilePath = try {
            parent.getChildText(MAIN_FILE)
        }
        catch (e: NullPointerException) {
            null
        }
        mainFile = if (!mainFilePath.isNullOrBlank()) {
            LocalFileSystem.getInstance().findFileByPath(mainFilePath)
        }
        else {
            null
        }

        val commandLineArgsText = try {
            parent.getChildText(COMMAND_LINE_ARGS)
        }
        catch (e: NullPointerException) {
            null
        }
        commandLineArguments = if (!commandLineArgsText.isNullOrBlank()) {
            commandLineArgsText
        }
        else {
            null
        }

        val workDirPath = try {
            parent.getChildText(WORK_DIR)
        }
        catch (e: NullPointerException) {
            null
        }
        workingDirectory = if (!workDirPath.isNullOrBlank()) {
            LocalFileSystem.getInstance().findFileByPath(workDirPath)
        }
        else {
            null
        }
    }

    override fun writeExternal(element: Element) {
        super<LocatableConfiguration>.writeExternal(element)

        val parent = element.getChild(PARENT_ELEMENT) ?: Element(PARENT_ELEMENT).apply { element.addContent(this) }
        parent.removeContent()

        parent.addContent(Element(PROGRAM).apply { text = makeindexProgram.name })
        parent.addContent(Element(MAIN_FILE).apply { text = mainFile?.path ?: "" })
        parent.addContent(Element(COMMAND_LINE_ARGS).apply { text = commandLineArguments ?: "" })
        parent.addContent(Element(WORK_DIR).apply { text = workingDirectory?.path ?: "" })
    }

    override fun isGeneratedName() = name == suggestedName()

    override fun suggestedName() = mainFile?.nameWithoutExtension + " " + makeindexProgram.name.lowercase(Locale.getDefault())

    fun setSuggestedName() {
        name = suggestedName()
    }
}

package nl.hannahsten.texifyidea.run.makeindex

import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.ConfigurationType
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.run.latex.LatexConfigurationFactory
import javax.swing.Icon

/**
 * Type of [MakeindexRunConfiguration].
 */
class MakeindexRunConfigurationType : ConfigurationType {

    override fun getIcon(): Icon = TexifyIcons.BUILD

    override fun getConfigurationTypeDescription() = "Makeindex run configuration"

    override fun getId() = "MAKEINDEX_RUN_CONFIGURATION"

    override fun getDisplayName() = "Makeindex"

    override fun getConfigurationFactories(): Array<ConfigurationFactory> = arrayOf(LatexConfigurationFactory(this))
}

package nl.hannahsten.texifyidea.run.makeindex

import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileTypeDescriptor
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.*
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.ui.RawCommandLineEditor
import nl.hannahsten.texifyidea.run.compiler.MakeindexProgram
import javax.swing.JComponent
import javax.swing.JPanel

/**
 * @author Thomas Schouten
 */
class MakeindexSettingsEditor(private val project: Project) : SettingsEditor<MakeindexRunConfiguration>() {

    private lateinit var panel: JPanel
    private lateinit var makeindexProgram: LabeledComponent<ComboBox<MakeindexProgram>>
    private lateinit var mainFile: LabeledComponent<TextFieldWithBrowseButton>
    private lateinit var commandLineArguments: LabeledComponent<RawCommandLineEditor>
    private lateinit var workingDirectory: LabeledComponent<TextFieldWithBrowseButton>

    override fun createEditor(): JComponent {
        createUIComponents()
        return panel
    }

    // Initialize editor from given run config
    override fun resetEditorFrom(runConfig: MakeindexRunConfiguration) {
        makeindexProgram.component.selectedItem = runConfig.makeindexProgram
        mainFile.component.text = runConfig.mainFile?.path ?: ""
        commandLineArguments.component.text = runConfig.commandLineArguments
        workingDirectory.component.text = runConfig.workingDirectory?.path ?: ""
    }

    // Save user selected settings to the given run config
    override fun applyEditorTo(runConfig: MakeindexRunConfiguration) {
        runConfig.makeindexProgram = makeindexProgram.component.selectedItem as MakeindexProgram
        runConfig.mainFile = LocalFileSystem.getInstance().findFileByPath(mainFile.component.text)
        runConfig.commandLineArguments = commandLineArguments.component.text
        runConfig.workingDirectory = LocalFileSystem.getInstance().findFileByPath(workingDirectory.component.text)
    }

    private fun createUIComponents() {
        panel = JPanel().apply {
            layout = VerticalFlowLayout(VerticalFlowLayout.TOP)

            // Program
            val programField = ComboBox(MakeindexProgram.values())
            makeindexProgram = LabeledComponent.create(programField, "Index program")
            add(makeindexProgram)

            // Main file
            val mainFileField = TextFieldWithBrowseButton().apply {
                addBrowseFolderListener(
                    TextBrowseFolderListener(
                        FileTypeDescriptor("Choose the Main .tex File", ".tex")
                            .withRoots(*ProjectRootManager.getInstance(project).contentRootsFromAllModules)
                    )
                )
            }
            mainFile = LabeledComponent.create(mainFileField, "Main file which uses an index")
            add(mainFile)

            commandLineArguments = LabeledComponent.create(RawCommandLineEditor(), "Custom arguments")
            add(commandLineArguments)

            // Working directory
            val workDirField = TextFieldWithBrowseButton().apply {
                addBrowseFolderListener(
                    TextBrowseFolderListener(
                        FileChooserDescriptor(false, true, false, false, false, false)
                            .withTitle("Choose the Directory Where the Index .idx File Will Be Generated")
                    )
                )
            }
            workingDirectory = LabeledComponent.create(workDirField, "Working directory for the index program")
            add(workingDirectory)
        }
    }
}

package nl.hannahsten.texifyidea.run.makeindex

import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.impl.RunConfigurationBeforeRunProvider
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.io.FileUtil
import nl.hannahsten.texifyidea.run.compiler.MakeindexProgram
import nl.hannahsten.texifyidea.run.latex.LatexConfigurationFactory
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.run.latex.getDefaultMakeindexPrograms
import nl.hannahsten.texifyidea.run.latex.getMakeindexOptions
import nl.hannahsten.texifyidea.util.appendExtension
import nl.hannahsten.texifyidea.util.magic.FileMagic
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException

/**
 * Run makeindex and then latex again (twice).
 */
class RunMakeindexListener(
    private val latexRunConfig: LatexRunConfiguration,
    private val environment: ExecutionEnvironment,
    private val filesToCleanUp: MutableList<File>
) : ProcessListener {

    override fun processTerminated(event: ProcessEvent) {
        try {
            // Only create new one if there is none yet
            val runConfigSettingsList =
                latexRunConfig.makeindexRunConfigs.ifEmpty {
                    generateIndexConfigs()
                }

            // Run all run configurations
            for (runConfigSettings in runConfigSettingsList) {
                val makeindexRunConfig = runConfigSettings.configuration as MakeindexRunConfiguration
                val options = getMakeindexOptions(makeindexRunConfig.mainFile, makeindexRunConfig.project)
                val baseFileName = options.getOrDefault("name", makeindexRunConfig.mainFile?.nameWithoutExtension) ?: return

                // Because bib2gls needs the bib file, and we do not want to guess the location of the bib file(s) to copy,
                // we choose to copy the aux file first and then run bib2gls next to the main file
                // In comparison: other makeindex programs we run in the auxil dir and then copy the output files (like .ind)
                if (makeindexRunConfig.makeindexProgram == MakeindexProgram.BIB2GLS) {
                    copyIndexOutputFiles(baseFileName, FileMagic.bib2glsDependenciesExtensions)
                }

                // Run makeindex
                RunConfigurationBeforeRunProvider.doExecuteTask(environment, runConfigSettings, null)

                copyIndexOutputFiles(baseFileName, FileMagic.indexFileExtensions)
                copyBib2glsOutput(makeindexRunConfig, baseFileName)
            }

            scheduleLatexRuns()
        }
        finally {
            latexRunConfig.isLastRunConfig = false
            latexRunConfig.isFirstRunConfig = true
        }
    }

    /**
     * .glstex is generated by bib2gls, we could clean it up but copy to output folder instead just in case
     */
    private fun copyBib2glsOutput(makeindexRunConfig: MakeindexRunConfiguration, baseFileName: String) {
        if (makeindexRunConfig.makeindexProgram == MakeindexProgram.BIB2GLS) {
            try {
                val mainFile = latexRunConfig.mainFile
                val auxilDir = latexRunConfig.getAuxilDirectory()
                if (mainFile != null && auxilDir != null && mainFile.path != auxilDir.path) {
                    for (extension in setOf("glstex", "glg")) {
                        FileUtil.rename(File(mainFile.parent.path + '/' + baseFileName.appendExtension(extension)), File(auxilDir.path + '/' + baseFileName.appendExtension(extension)))
                    }
                }
            }
            catch (ignored: IOException) {}
        }
    }

    private fun scheduleLatexRuns() {
        // Don't schedule more latex runs if bibtex is used, because that will already schedule the extra runs
        if (latexRunConfig.bibRunConfigs.isEmpty()) {
            // LaTeX twice
            latexRunConfig.isFirstRunConfig = false
            val latexSettings = RunManagerImpl.getInstanceImpl(environment.project).getSettings(latexRunConfig)
                ?: return
            latexRunConfig.isLastRunConfig = false
            RunConfigurationBeforeRunProvider.doExecuteTask(environment, latexSettings, null)
            latexRunConfig.isLastRunConfig = true
            RunConfigurationBeforeRunProvider.doExecuteTask(environment, latexSettings, null)
        }
    }

    private fun generateIndexConfigs(): Set<RunnerAndConfigurationSettings> {
        val runManager = RunManagerImpl.getInstanceImpl(environment.project)
        val indexPrograms = getDefaultMakeindexPrograms(latexRunConfig.mainFile, environment.project)

        val runConfigs = mutableSetOf<RunnerAndConfigurationSettings>()

        for (indexProgram in indexPrograms) {
            val makeindexRunConfigSettings = runManager.createConfiguration(
                "",
                LatexConfigurationFactory(MakeindexRunConfigurationType())
            )

            val runConfig = makeindexRunConfigSettings.configuration as MakeindexRunConfiguration

            runConfig.mainFile = latexRunConfig.mainFile
            runConfig.makeindexProgram = indexProgram
            runConfig.setSuggestedName()

            // bib2gls we handle separately, because it needs the bib file, so instead of running it in the auxil dir we run it next to the main file
            runConfig.workingDirectory = if (runConfig.makeindexProgram != MakeindexProgram.BIB2GLS) latexRunConfig.getAuxilDirectory() else runConfig.mainFile?.parent

            runManager.addConfiguration(makeindexRunConfigSettings)
            runConfigs.add(makeindexRunConfigSettings)
        }

        latexRunConfig.makeindexRunConfigs = runConfigs
        return runConfigs
    }

    /**
     * Copy .ind and similar files from where they were generated by makeindex to next to the main file, so an index package will hopefully find it.
     *
     * @param baseFileName Filename of generated files without extension.
     *
     * @return List of files that should be cleaned up.
     */
    private fun copyIndexOutputFiles(baseFileName: String, indexFileExtensions: Set<String>) {
        val mainFile = latexRunConfig.mainFile ?: return
        val auxilDir = latexRunConfig.getAuxilDirectory() ?: return
        val sourceDir = auxilDir.path + '/'
        val destinationDir = File(mainFile.path).parent + '/'

        if (sourceDir == destinationDir) return

        // Just try all extensions for files that need to be copied
        // Skips files that do not exist
        for (extension in indexFileExtensions) {
            val indexFileName = baseFileName.appendExtension(extension)
            val indexFileSource = File(sourceDir + indexFileName)
            if (!indexFileSource.isFile) continue
            val indexFileDestination = File(destinationDir + indexFileName)

            // Only clean up file if not copied there by user
            if (indexFileSource.exists() && !indexFileDestination.exists()) {
                filesToCleanUp.add(indexFileDestination)
            }

            try {
                FileUtil.copy(indexFileSource, indexFileDestination)
            }
            catch (ignored: FileNotFoundException) {
            }
        }
    }

    override fun onTextAvailable(p0: ProcessEvent, p1: Key<*>) {}

    override fun processWillTerminate(p0: ProcessEvent, p1: Boolean) {}

    override fun startNotified(p0: ProcessEvent) {}
}

package nl.hannahsten.texifyidea.run.pdfviewer

import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project

/**
 * Allow other plugins to define their own pdf viewers.
 *
 * We also use this extension point to define the Intellij internal pdf viewer, to make sure that
 * TeXiFy only uses it when the (optional dependency) IntelliJ PDF Viewer plugin is installed.
 */
private val EP_NAME = ExtensionPointName<ExternalPdfViewer>("nl.rubensten.texifyidea.pdfViewer")

/**
 * Interface that defines a pdf viewer so we can use both [nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer]s and [ExternalPdfViewer]s interchangeably.
 */
interface PdfViewer {

    val name: String?
    val displayName: String?

    fun isAvailable(): Boolean
}

/**
 * Define behaviour that external pdf viewers should inherit.
 */
interface ExternalPdfViewer : PdfViewer {

    fun forwardSearch(pdfPath: String?, sourceFilePath: String, line: Int, project: Project, focusAllowed: Boolean)
}

/**
 * Define functions that handle all external pdf viewers one by one.
 */
object ExternalPdfViewers {

    fun getExternalPdfViewers(): List<ExternalPdfViewer> = EP_NAME.extensionList
}

package nl.hannahsten.texifyidea.run.sumatra

import com.intellij.openapi.util.SystemInfo
import com.pretty_tools.dde.client.DDEClientConversation
import nl.hannahsten.texifyidea.util.Log
import nl.hannahsten.texifyidea.util.runCommand
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import java.io.File

/**
 * Indicates whether SumatraPDF is installed and DDE communication is enabled.
 *
 * Is computed once at initialization (for performance), which means that the IDE needs to be restarted when users
 * install SumatraPDF while running TeXiFy.
 */
object SumatraAvailabilityChecker {

    var isSumatraAvailable: Boolean = isSumatraInstalledAndAvailable()
        private set

    /** If we know a valid path containing SumatraPDF.exe, it will be stored here, in case as a last resort you really just want to open a Sumatra, doesn't matter which one. */
    var sumatraDirectory: File? = null

    private fun isSumatraInstalledAndAvailable(): Boolean {
        if (!SystemInfo.isWindows || !isSumatraInstalled()) return false

        // Try if native bindings are available
        // Note: this will not throw any exception when Sumatra is not installed, that will only happen when we really try to connect()
        try {
            DDEClientConversation()
        }
        catch (e: UnsatisfiedLinkError) {
            Log.info("Native library DLLs could not be found.")
            return false
        }
        catch (e: NoClassDefFoundError) {
            Log.info("Native library DLLs could not be found.")
            return false
        }

        return true
    }

    /**
     * Checks if Sumatra can be found in a global PATH or in a directory (with sumatraCustomPath)
     * Verifies that sumatraCustomPath is a directory, non-null and non-empty before checking in the directory for Sumatra.
     * Updates sumatraWorkingCustomDir and isSumatraAvailable if assignNewAvailability is true
     * Returns a pair, first tells if Sumatra is accessible in PATH or customPath, seconds tells if Sumatra is
     * accessible in customPath
     */
    fun isSumatraPathAvailable(
        sumatraCustomPath: String? = null,
        assignNewAvailability: Boolean = true
    ): Pair<Boolean, Boolean> {
        var workingDir: File? = null
        if (!sumatraCustomPath.isNullOrEmpty() && File(sumatraCustomPath).isDirectory) {
            workingDir = File(sumatraCustomPath)
        }

        var isCustomPathValid = false // if Sumatra is accessible in customPath
        val whereSumatraResult = runCommandWithExitCode("where", "SumatraPDF", workingDirectory = workingDir)
        val isSumatraInAllPath = (whereSumatraResult.second == 0) // if Sumatra is accessible in PATH or customPath

        if (workingDir != null && whereSumatraResult.first?.contains(sumatraCustomPath.toString()) == true && isSumatraInAllPath) {
            isCustomPathValid = true
        }

        if (assignNewAvailability) {
            if (!isSumatraAvailable) {
                isSumatraAvailable = isSumatraInAllPath
            }
            if (workingDir != null) {
                sumatraDirectory = workingDir
            }
        }

        return Pair(isSumatraInAllPath, isCustomPathValid)
    }

    /**
     * Checks at initialization if the Sumatra registry keys are registered or if Sumatra is in PATH.
     * returns true if the Sumatra registry keys are registered or if Sumatra is in PATH.
     */
    private fun isSumatraInstalled(): Boolean {
        // Try if Sumatra is in PATH
        val guessedPaths = listOf(
            null,
            "${System.getenv("HOMEDRIVE")}${System.getenv("HOMEPATH")}AppData\\Local\\SumatraPDF",
            "C:\\Users\\${System.getenv("USERNAME")}\\AppData\\Local\\SumatraPDF",
        )
        for (path in guessedPaths) {
            if (isSumatraPathAvailable(sumatraCustomPath = path, assignNewAvailability = true).first) {
                return true
            }
        }

        // Try some SumatraPDF registry keys
        // For some reason this first one isn't always present anymore, it used to be
        val paths = listOf(
            "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\SumatraPDF.exe",
            "HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\SumatraPDF.pdf",
            "HKEY_CURRENT_USER\\SOFTWARE\\Classes\\SumatraPDF.pdf",
        )
        for (path in paths) {
            if (runCommand("reg", "query", path, "/ve")?.startsWith("ERROR:") == false) {
                // To improve this, we could also update the known installation directory so that we can open Sumatra automatically
                return true
            }
        }

        // https://github.com/sumatrapdfreader/sumatrapdf/discussions/2855#discussioncomment-3336646

        // We could also look at the values of the following reg keys to find the install path:
        // [HKEY_CURRENT_USER\Software\Classes\SumatraPDF.pdf\shell\open]
        // "Icon"="C:\\Users\\K\\AppData\\Local\\SumatraPDF\\SumatraPDF.exe"
        //
        // [HKEY_CURRENT_USER\Software\Classes\SumatraPDF.pdf\shell\open\command]
        // @="\"C:\\Users\\K\\AppData\\Local\\SumatraPDF\\SumatraPDF.exe\" \"%1\""

        return false
    }
}

package nl.hannahsten.texifyidea.run.sumatra

import com.intellij.openapi.project.Project
import com.pretty_tools.dde.client.DDEClientConversation
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.run.linuxpdfviewer.ViewerConversation
import nl.hannahsten.texifyidea.util.runCommandWithExitCode

/**
 * Send commands to SumatraPDF.
 *
 * This is available on Windows only.
 *
 * @author Sten Wessel
 * @since b0.4
 */
object SumatraConversation : ViewerConversation() {

    private const val server = "SUMATRA"
    private const val topic = "control"
    private var conversation: DDEClientConversation? = null

    private fun openConversation() {
        if (SumatraAvailabilityChecker.isSumatraAvailable && conversation == null) {
            try {
                conversation = DDEClientConversation()
            }
            catch (e: NoClassDefFoundError) {
                throw TeXception("Native library DLLs could not be found.", e)
            }
        }
    }

    /**
     * Open a file in SumatraPDF, starting it if it is not running yet.
     */
    fun openFile(pdfFilePath: String, newWindow: Boolean = false, focus: Boolean = false, forceRefresh: Boolean = false) {
        try {
            execute("Open(\"$pdfFilePath\", ${newWindow.bit}, ${focus.bit}, ${forceRefresh.bit})")
        }
        catch (e: TeXception) {
            // Make sure Windows popup error doesn't appear and we will still open Sumatra
            if (SumatraAvailabilityChecker.isSumatraAvailable) {
                runCommandWithExitCode("cmd.exe", "/C", "start", "SumatraPDF", "-reuse-instance", pdfFilePath, workingDirectory = SumatraAvailabilityChecker.sumatraDirectory, nonBlocking = true)
            }
        }
    }

    override fun forwardSearch(pdfPath: String?, sourceFilePath: String, line: Int, project: Project, focusAllowed: Boolean) {
        forwardSearch(pdfPath, sourceFilePath, line, focus = focusAllowed)
    }

    /**
     * Execute forward search, highlighting a certain line in SumatraPDF.
     */
    fun forwardSearch(pdfFilePath: String? = null, sourceFilePath: String, line: Int, newWindow: Boolean = false, focus: Boolean = false) {
        val pdfPath = if (pdfFilePath != null) "\"$pdfFilePath\", " else ""
        execute("ForwardSearch($pdfPath\"$sourceFilePath\", $line, 0, ${newWindow.bit}, ${focus.bit})")
    }

    fun gotoNamedDest(pdfFilePath: String, destinationName: String) {
        execute("GotoNamedDest(\"$pdfFilePath\", \"$destinationName\")")
    }

    fun gotoPage(pdfFilePath: String, page: Int) {
        execute("GotoPage(\"$pdfFilePath\", $page)")
    }

    fun setView(pdfFilePath: String, viewMode: ViewMode, zoomLevel: ZoomLevel, scroll: Pair<Int, Int>? = null) {
        val s = if (scroll != null) ", ${scroll.first}, ${scroll.second}" else ""
        execute("SetView(\"$pdfFilePath\", \"${viewMode.description}\", ${zoomLevel.percentage}$s)")
    }

    private fun execute(vararg commands: String) {
        openConversation()
        try {
            conversation!!.connect(server, topic)
            conversation!!.execute(commands.joinToString(separator = "") { "[$it]" })
        }
        catch (e: Exception) {
            throw TeXception("Connection to SumatraPDF was disrupted.", e)
        }
        finally {
            conversation?.disconnect()
        }
    }

    /**
     * @author Sten Wessel
     */
    enum class ViewMode(val description: String) {

        SINGLE_PAGE("single page"),
        FACING("facing"),
        BOOK_VIEW("book view"),
        CONTINUOUS("continuous"),
        CONTINUOUS_FACING("continuous facing"),
        CONTINUOUS_BOOK_VIEW("continuous book view");
    }

    /**
     * @author Sten Wessel
     */
    class ZoomLevel(val percentage: Int) {

        companion object {

            private val fitPage = ZoomLevel(-1)
            private val fitWidth = ZoomLevel(-2)
            private val fitContent = ZoomLevel(-3)
        }

        init {
            require(percentage in 8..6400) { "Percentage must be in range [8,6400]" }
        }
    }
}

private val Boolean.bit: Int
    get() = if (this) 1 else 0

package nl.hannahsten.texifyidea.run.sumatra

import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.util.Key
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.action.ForwardSearchAction
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer
import nl.hannahsten.texifyidea.util.caretOffset
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.isRoot
import nl.hannahsten.texifyidea.util.files.openedTextEditor
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.parser.name
import nl.hannahsten.texifyidea.util.parser.parentOfType
import org.jetbrains.concurrency.runAsync

/**
 * @author Sten Wessel
 */
class SumatraForwardSearchListener(
    val runConfig: LatexRunConfiguration,
    private val executionEnvironment: ExecutionEnvironment
) : ProcessListener {

    override fun processTerminated(event: ProcessEvent) {
        // First check if the user provided a custom path to SumatraPDF, if not, check if it is installed
        if (event.exitCode == 0 && SumatraAvailabilityChecker.isSumatraAvailable) {
            try {
                SumatraConversation.openFile(runConfig.outputFilePath)
            }
            catch (ignored: TeXception) {
            }
        }

        (ActionManager.getInstance().getAction("texify.ForwardSearch") as? ForwardSearchAction)?.viewer = InternalPdfViewer.SUMATRA

        // Forward search.
        invokeLater {
            val psiFile = runConfig.mainFile?.psiFile(executionEnvironment.project) ?: return@invokeLater
            val document = psiFile.document() ?: return@invokeLater

            val editor = psiFile.openedTextEditor() ?: return@invokeLater

            if (document != editor.document) {
                return@invokeLater
            }

            // Do not do forward search when editing the preamble.
            if (psiFile.isRoot()) {
                val element = psiFile.findElementAt(editor.caretOffset()) ?: return@invokeLater
                val latexEnvironment = element.parentOfType(LatexEnvironment::class) ?: return@invokeLater
                if (latexEnvironment.name()?.text != "document") {
                    return@invokeLater
                }
            }

            val line = document.getLineNumber(editor.caretOffset()) + 1

            runAsync {
                try {
                    // Wait for sumatra pdf to start. 1250ms should be plenty.
                    // Otherwise the person is out of luck ¯\_(ツ)_/¯
                    Thread.sleep(1250)
                    // Never focus, because forward search will work fine without focus, and the user might want to continue typing after doing forward search/compiling
                    SumatraConversation.forwardSearch(sourceFilePath = psiFile.virtualFile.path, line = line, focus = false)
                }
                catch (ignored: TeXception) {
                }
            }
        }

        // Reset to default
        runConfig.allowFocusChange = true
    }

    override fun onTextAvailable(p0: ProcessEvent, p1: Key<*>) {
        // Do nothing.
    }

    override fun processWillTerminate(p0: ProcessEvent, p1: Boolean) {
        // Do nothing.
    }

    override fun startNotified(p0: ProcessEvent) {
        // Do nothing.
    }
}

package nl.hannahsten.texifyidea.settings.codestyle

import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CustomCodeStyleSettings
import nl.hannahsten.texifyidea.grammar.BibtexLanguage

class BibtexCodeStyleSettings(container: CodeStyleSettings) : CustomCodeStyleSettings(BibtexLanguage.id, container)

package nl.hannahsten.texifyidea.settings.codestyle

import com.intellij.application.options.CodeStyleAbstractConfigurable
import com.intellij.application.options.CodeStyleAbstractPanel
import com.intellij.application.options.TabbedLanguageCodeStylePanel
import com.intellij.psi.codeStyle.CodeStyleConfigurable
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CodeStyleSettingsProvider
import nl.hannahsten.texifyidea.grammar.BibtexLanguage

class BibtexCodeStyleSettingsProvider : CodeStyleSettingsProvider() {

    override fun createCustomSettings(settings: CodeStyleSettings) = BibtexCodeStyleSettings(settings)

    override fun getConfigurableDisplayName() = BibtexLanguage.displayName

    override fun createConfigurable(settings: CodeStyleSettings, originalSettings: CodeStyleSettings): CodeStyleConfigurable {
        return object : CodeStyleAbstractConfigurable(settings, originalSettings, configurableDisplayName) {

            override fun createPanel(settings: CodeStyleSettings): CodeStyleAbstractPanel {
                val language = BibtexLanguage

                return object : TabbedLanguageCodeStylePanel(language, currentSettings, settings) {

                    override fun initTabs(settings: CodeStyleSettings) {
                        addIndentOptionsTab(settings)
                        addWrappingAndBracesTab(settings)
                    }

                    override fun addWrappingAndBracesTab(settings: CodeStyleSettings?) {
                        addTab(object : MyWrappingAndBracesPanel(settings) {
                            // Remove "Braces" from tab title
                            override fun getTabTitle() = "Wrapping"
                        })
                    }
                }
            }
        }
    }
}

package nl.hannahsten.texifyidea.settings.codestyle

import com.intellij.application.options.SmartIndentOptionsEditor
import com.intellij.lang.Language
import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable
import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider
import com.intellij.psi.codeStyle.extractor.values.Value
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.util.magic.GeneralMagic
import nl.hannahsten.texifyidea.util.removeHtmlTags

class BibtexLanguageCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider() {

    companion object {

        private val demoText = GeneralMagic.bibtexDemoText.removeHtmlTags()
    }

    override fun getLanguage(): Language = BibtexLanguage

    override fun getCodeSample(settingsType: SettingsType): String = demoText

    override fun getIndentOptionsEditor() = SmartIndentOptionsEditor()

    override fun customizeSettings(consumer: CodeStyleSettingsCustomizable, settingsType: SettingsType) {
        when (settingsType) {
            SettingsType.WRAPPING_AND_BRACES_SETTINGS -> customizeWrappingAndBracesSettings(consumer)
            else -> return
        }
    }

    private fun customizeWrappingAndBracesSettings(consumer: CodeStyleSettingsCustomizable) {
        consumer.showStandardOptions(
            *arrayOf(
                Value.VAR_KIND.RIGHT_MARGIN,
                CodeStyleSettingsCustomizable.WrappingOrBraceOption.WRAP_ON_TYPING,
                CodeStyleSettingsCustomizable.WrappingOrBraceOption.WRAP_LONG_LINES,
                CodeStyleSettingsCustomizable.WrappingOrBraceOption.KEEP_FIRST_COLUMN_COMMENT
            ).map { it.toString() }.toTypedArray()
        )
    }
}

package nl.hannahsten.texifyidea.settings.codestyle

import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CustomCodeStyleSettings
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.util.magic.cmd

/**
 * Defines all the custom code style settings.
 *
 * @author Abby Berkers
 */
@Suppress("PropertyName")
class LatexCodeStyleSettings(container: CodeStyleSettings) : CustomCodeStyleSettings(LatexLanguage.id, container) {

    /**
     * The number of blank lines to use before a sectioning command.
     */
    // JvmField is required to avoid NoSuchFieldException when referring to the field name in LatexLanguageCodeStyleSettingsProvider
    @JvmField var BLANK_LINES_BEFORE_PART: Int = 2

    @JvmField var BLANK_LINES_BEFORE_CHAPTER: Int = 2

    @JvmField var BLANK_LINES_BEFORE_SECTION: Int = 2

    @JvmField var BLANK_LINES_BEFORE_SUBSECTION: Int = 1

    @JvmField var BLANK_LINES_BEFORE_SUBSUBSECTION: Int = 1

    @JvmField var BLANK_LINES_BEFORE_PARAGRAPH: Int = 1

    @JvmField var BLANK_LINES_BEFORE_SUBPARAGRAPH: Int = 1

    /**
     * Indent text inside sections.
     */
    @JvmField var INDENT_SECTIONS = false

    /**
     * Indent the document environment as a normal environment.
     */
    @JvmField var INDENT_DOCUMENT_ENVIRONMENT = true

    companion object {

        val blankLinesOptions = mapOf(
            LatexCodeStyleSettings::BLANK_LINES_BEFORE_PART to LatexGenericRegularCommand.PART.cmd,
            LatexCodeStyleSettings::BLANK_LINES_BEFORE_CHAPTER to LatexGenericRegularCommand.CHAPTER.cmd,
            LatexCodeStyleSettings::BLANK_LINES_BEFORE_SECTION to LatexGenericRegularCommand.SECTION.cmd,
            LatexCodeStyleSettings::BLANK_LINES_BEFORE_SUBSECTION to LatexGenericRegularCommand.SUBSECTION.cmd,
            LatexCodeStyleSettings::BLANK_LINES_BEFORE_SUBSUBSECTION to LatexGenericRegularCommand.SUBSUBSECTION.cmd,
            LatexCodeStyleSettings::BLANK_LINES_BEFORE_PARAGRAPH to LatexGenericRegularCommand.PARAGRAPH.cmd,
            LatexCodeStyleSettings::BLANK_LINES_BEFORE_SUBPARAGRAPH to LatexGenericRegularCommand.SUBPARAGRAPH.cmd
        )
    }
}

package nl.hannahsten.texifyidea.settings.codestyle

import com.intellij.application.options.CodeStyleAbstractConfigurable
import com.intellij.application.options.CodeStyleAbstractPanel
import com.intellij.application.options.TabbedLanguageCodeStylePanel
import com.intellij.psi.codeStyle.CodeStyleConfigurable
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CodeStyleSettingsProvider
import nl.hannahsten.texifyidea.grammar.LatexLanguage

/**
 * Provides the LaTeX code style settings.
 *
 * The most useful thing here is that we specify which tabs to show in the
 * settings.
 *
 * @author Sten Wessel
 */
class LatexCodeStyleSettingsProvider : CodeStyleSettingsProvider() {

    override fun createCustomSettings(settings: CodeStyleSettings) = LatexCodeStyleSettings(settings)

    override fun getConfigurableDisplayName() = LatexLanguage.displayName

    override fun createConfigurable(settings: CodeStyleSettings, originalSettings: CodeStyleSettings): CodeStyleConfigurable {
        return object : CodeStyleAbstractConfigurable(settings, originalSettings, configurableDisplayName) {

            override fun createPanel(settings: CodeStyleSettings): CodeStyleAbstractPanel {
                val language = LatexLanguage

                return object : TabbedLanguageCodeStylePanel(language, currentSettings, settings) {

                    override fun initTabs(settings: CodeStyleSettings) {
                        addIndentOptionsTab(settings)
                        addWrappingAndBracesTab(settings)
                        addBlankLinesTab(settings)

                        // Adds the Code Generation tab.
                        for (provider in EXTENSION_POINT_NAME.extensions) {
                            if (provider.language === LatexLanguage && !provider.hasSettingsPage()) {
                                createTab(provider)
                            }
                        }
                    }

                    override fun addWrappingAndBracesTab(settings: CodeStyleSettings?) {
                        addTab(object : MyWrappingAndBracesPanel(settings) {
                            // Remove "Braces" from tab title
                            override fun getTabTitle() = "Wrapping"
                        })
                    }
                }
            }
        }
    }
}

package nl.hannahsten.texifyidea.settings.codestyle

import com.intellij.application.options.codeStyle.CommenterForm
import com.intellij.openapi.application.ApplicationBundle
import com.intellij.psi.codeStyle.CodeStyleConfigurable
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CodeStyleSettingsProvider
import com.intellij.psi.codeStyle.DisplayPriority
import com.intellij.ui.IdeBorderFactory
import com.intellij.util.ui.JBInsets
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import javax.swing.BoxLayout
import javax.swing.JComponent
import javax.swing.JPanel

/**
 * Copied from the Kotlin plugin.
 *
 * @author Abby Berkers
 */
class LatexGenerationSettingsProvider : CodeStyleSettingsProvider() {

    override fun createConfigurable(settings: CodeStyleSettings, modelSettings: CodeStyleSettings): CodeStyleConfigurable {
        return LatexCodeStyleGenerationConfigurable(settings)
    }

    override fun getConfigurableDisplayName(): String = ApplicationBundle.message("title.code.generation")
    override fun getPriority(): DisplayPriority = DisplayPriority.CODE_SETTINGS
    override fun hasSettingsPage() = false
    override fun getLanguage(): LatexLanguage = LatexLanguage
}

class LatexCodeStyleGenerationConfigurable(private val mySettings: CodeStyleSettings) : CodeStyleConfigurable {

    private val myCommenterForm: CommenterForm = CommenterForm(LatexLanguage)

    override fun getDisplayName(): String = ApplicationBundle.message("title.code.generation")

    override fun createComponent(): JComponent {
        return JPanel().apply {
            layout = BoxLayout(this, BoxLayout.Y_AXIS)
            border = IdeBorderFactory.createEmptyBorder(JBInsets(0, 10, 10, 10))
            add(myCommenterForm.commenterPanel)
        }
    }

    override fun isModified(): Boolean {
        return myCommenterForm.isModified(mySettings)
    }

    override fun apply() {
        apply(mySettings)
    }

    override fun reset() {
        reset(mySettings)
    }

    override fun reset(settings: CodeStyleSettings) {
        myCommenterForm.reset(settings)
    }

    override fun apply(settings: CodeStyleSettings) {
        myCommenterForm.apply(settings)
    }
}

package nl.hannahsten.texifyidea.settings.codestyle

import com.intellij.application.options.IndentOptionsEditor
import com.intellij.application.options.SmartIndentOptionsEditor
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CommonCodeStyleSettings
import javax.swing.JCheckBox

/**
 * Customizing the 'Tabs and Indents' settings cannot be done via [LatexLanguageCodeStyleSettingsProvider.customizeSettings] using INDENT_SETTINGS but has to be done here.
 *
 * See e.g. the YAML plugin "YAMLLanguageCodeStyleSettingsProvider"
 */
class LatexIndentOptionsEditor(provider: LatexLanguageCodeStyleSettingsProvider) : SmartIndentOptionsEditor(provider) {

    private val sectionIndents = JCheckBox("Nested indent of sections")
    private val documentIndent = JCheckBox("Indent document environment")

    override fun addComponents() {
        super.addComponents()
        add(sectionIndents)
        add(documentIndent)
    }

    override fun setEnabled(enabled: Boolean) {
        super.setEnabled(enabled)
        sectionIndents.isEnabled = enabled
        documentIndent.isEnabled = enabled
    }

    override fun isModified(settings: CodeStyleSettings?, options: CommonCodeStyleSettings.IndentOptions?): Boolean {
        val isModified = super.isModified(settings, options)
        val latexSettings = settings?.getCustomSettings(LatexCodeStyleSettings::class.java) ?: return false
        return isModified ||
            IndentOptionsEditor.isFieldModified(sectionIndents, latexSettings.INDENT_SECTIONS) ||
            IndentOptionsEditor.isFieldModified(documentIndent, latexSettings.INDENT_DOCUMENT_ENVIRONMENT)
    }

    override fun apply(settings: CodeStyleSettings?, options: CommonCodeStyleSettings.IndentOptions?) {
        super.apply(settings, options)

        val latexSettings = settings?.getCustomSettings(LatexCodeStyleSettings::class.java)
        latexSettings?.INDENT_SECTIONS = sectionIndents.isSelected
        latexSettings?.INDENT_DOCUMENT_ENVIRONMENT = documentIndent.isSelected
    }

    override fun reset(settings: CodeStyleSettings, options: CommonCodeStyleSettings.IndentOptions) {
        super.reset(settings, options)

        val latexSettings = settings.getCustomSettings(LatexCodeStyleSettings::class.java)
        sectionIndents.isSelected = latexSettings.INDENT_SECTIONS
        documentIndent.isSelected = latexSettings.INDENT_DOCUMENT_ENVIRONMENT
    }
}

package nl.hannahsten.texifyidea.settings.codestyle

import com.intellij.openapi.application.ApplicationBundle
import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable
import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable.CommenterOption.LINE_COMMENT_ADD_SPACE
import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable.CommenterOption.LINE_COMMENT_AT_FIRST_COLUMN
import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable.WrappingOrBraceOption.*
import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider
import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider.SettingsType.*
import com.intellij.psi.codeStyle.extractor.values.Value.VAR_KIND.RIGHT_MARGIN
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.util.magic.GeneralMagic
import nl.hannahsten.texifyidea.util.removeHtmlTags

/**
 * Provides the code style settings
 * @author Sten Wessel
 */
class LatexLanguageCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider() {

    companion object {

        private val demoText = GeneralMagic.latexDemoText.removeHtmlTags()
    }

    override fun getLanguage() = LatexLanguage

    override fun getCodeSample(settingsType: SettingsType) = demoText

    override fun getIndentOptionsEditor() = LatexIndentOptionsEditor(this)

    override fun customizeSettings(consumer: CodeStyleSettingsCustomizable, settingsType: SettingsType) {
        when (settingsType) {
            WRAPPING_AND_BRACES_SETTINGS -> customizeWrappingAndBracesSettings(consumer)
            BLANK_LINES_SETTINGS -> customizeBlankLinesSettings(consumer)
            COMMENTER_SETTINGS -> customizeCodeGenerationSettings(consumer)
            else -> return
        }
    }

    private fun customizeWrappingAndBracesSettings(consumer: CodeStyleSettingsCustomizable) {
        consumer.showStandardOptions(
            *arrayOf(
                RIGHT_MARGIN,
                WRAP_ON_TYPING,
                WRAP_LONG_LINES,
                KEEP_FIRST_COLUMN_COMMENT
            ).map { it.toString() }.toTypedArray()
        )
    }

    private fun customizeBlankLinesSettings(consumer: CodeStyleSettingsCustomizable) {
        LatexCodeStyleSettings.blankLinesOptions.forEach {
            consumer.showCustomOption(
                LatexCodeStyleSettings::class.java,
                it.key.name,
                it.value,
                ApplicationBundle.message("title.minimum.blank.lines")
            )
        }
    }

    private fun customizeCodeGenerationSettings(consumer: CodeStyleSettingsCustomizable) {
        consumer.showStandardOptions(
            *arrayOf(
                LINE_COMMENT_AT_FIRST_COLUMN,
                LINE_COMMENT_ADD_SPACE
            ).map { it.toString() }.toTypedArray()
        )
    }
}

package nl.hannahsten.texifyidea.settings.conventions

/**
 * A label convention stores the Texify conventions for a specific label type.
 *
 * The parameters need default values for XML serialization. Turning any of them into a "val" leads to the serialization
 * silently failing.
 */
data class LabelConvention(

    /**
     * Whether the convention is enabled
     */
    var enabled: Boolean = false,

    /**
     * The type of Latex element this convention applies to
     */
    var type: LabelConventionType? = null,

    /**
     * The command name this convention applies to
     */
    var name: String? = null,

    /**
     * The prefix to use for an inserted label
     */
    var prefix: String? = null
)

enum class LabelConventionType(private val prettyName: String) {

    ENVIRONMENT("Environment"),
    COMMAND("Command");

    override fun toString(): String {
        return prettyName
    }
}

package nl.hannahsten.texifyidea.settings.conventions

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.AnActionButton
import com.intellij.ui.JBIntSpinner
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.table.TableView
import com.intellij.ui.util.preferredWidth
import com.intellij.util.ui.AbstractTableCellEditor
import com.intellij.util.ui.JBDimension
import com.intellij.util.ui.ListTableModel
import com.intellij.util.ui.table.TableModelEditor
import nl.hannahsten.texifyidea.TexifyIcons
import java.awt.BorderLayout
import java.awt.Component
import java.awt.event.ItemEvent
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTable
import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.TableCellEditor
import javax.swing.table.TableCellRenderer

/**
 * Configurable for configuring Texify convention settings.
 *
 * The goal of this configurable is to make certain parts of the conventions currently hardcoded in the *Magic classes
 * configurable.
 *
 * The Configurable can manage settings for both, project and IDE scope. To manage the settings for the project scope,
 * the Configurable is registered as a project configurable. Changes made in the UI are transferred to a
 * [TexifyConventionsSettings] object.
 */
class TexifyConventionsConfigurable(project: Project) : SearchableConfigurable, Configurable.VariableProjectAppLevel {

    private val settingsManager: TexifyConventionsSettingsManager =
        TexifyConventionsSettingsManager.getInstance(project)

    /**
     * A settings instance to record active changes. This instance is copied to the actual instance whenever settings
     * should be applied.
     */
    private val unsavedSettings: TexifyConventionsSettings = TexifyConventionsSettings()

    // used for the selector
    private lateinit var labelCommandType: ComboBox<LabelConventionType>
    private lateinit var schemesPanel: TexifyConventionsSchemesPanel
    private lateinit var mainPanel: JPanel
    private lateinit var maxSectionSize: JBIntSpinner
    private lateinit var labelConventionsTable: TableView<LabelConvention>

    private val typeColumnInfo =
        object : TableModelEditor.EditableColumnInfo<LabelConvention, LabelConvention>("Type") {
            override fun valueOf(item: LabelConvention?): LabelConvention? = item
            override fun getColumnClass(): Class<*> = LabelConvention::class.java
            override fun getRenderer(item: LabelConvention?): TableCellRenderer {
                return object : DefaultTableCellRenderer() {
                    override fun getTableCellRendererComponent(
                        table: JTable?,
                        value: Any?,
                        selected: Boolean,
                        focus: Boolean,
                        row: Int,
                        column: Int
                    ): Component {
                        val convention = value as LabelConvention?
                        super.getTableCellRendererComponent(table, convention?.type, selected, focus, row, column)
                        //noinspection unchecked
                        icon = if (value != null) {
                            when (value.type!!) {
                                LabelConventionType.ENVIRONMENT -> TexifyIcons.DOT_ENVIRONMENT
                                LabelConventionType.COMMAND -> TexifyIcons.DOT_COMMAND
                            }
                        }
                        else {
                            null
                        }

                        return this
                    }
                }
            }

            override fun setValue(item: LabelConvention?, value: LabelConvention?) {
                if (item != null && value != null)
                    item.type = value.type
            }

            override fun getEditor(o: LabelConvention?): TableCellEditor {
                return object : AbstractTableCellEditor() {
                    override fun getCellEditorValue(): Any {
                        val labelType =
                            (labelCommandType.selectedItem ?: LabelConventionType.ENVIRONMENT) as LabelConventionType
                        return LabelConvention(false, labelType)
                    }

                    override fun getTableCellEditorComponent(
                        table: JTable,
                        value: Any,
                        isSelected: Boolean,
                        row: Int,
                        column: Int
                    ): Component {
                        if (value !is LabelConvention) {
                            labelCommandType.selectedItem = LabelConventionType.ENVIRONMENT
                        }
                        else {
                            labelCommandType.selectedItem = value.type
                        }
                        return labelCommandType
                    }
                }
            }
        }

    private val prefixColumnInfo = object : TableModelEditor.EditableColumnInfo<LabelConvention, String>("Prefix") {
        override fun valueOf(item: LabelConvention): String = item.prefix!!
        override fun getTooltipText(): String = "The prefix labels for the given Latex element should have"
        override fun setValue(item: LabelConvention, value: String?) {
            item.prefix = value ?: ""
        }
    }

    private val nameColumnInfo = object : TableModelEditor.EditableColumnInfo<LabelConvention, String>("Element") {
        override fun valueOf(item: LabelConvention): String = item.name!!
        override fun getColumnClass(): Class<*> = String::class.java
        override fun setValue(item: LabelConvention, value: String?) {
            item.name = value ?: ""
        }
    }

    private val enabledColumnInfo =
        object : TableModelEditor.EditableColumnInfo<LabelConvention, Boolean>("Should Have Label") {
            override fun getColumnClass(): Class<*> = Boolean::class.java
            override fun getTooltipText(): String =
                "If enabled, TeXiFy issues a warning if the given Latex element does not have a label"

            override fun valueOf(item: LabelConvention): Boolean = item.enabled
            override fun setValue(item: LabelConvention, value: Boolean) {
                item.enabled = value
            }

            override fun getMaxStringValue(): String? {
                // Give a hint that the column won't ever need more space than its label
                return super.getName()
            }
        }

    private fun createConventionsTable(): TableView<LabelConvention> {
        val model = ListTableModel<LabelConvention>(typeColumnInfo, nameColumnInfo, prefixColumnInfo, enabledColumnInfo)
        labelConventionsTable = TableView(model)

        return labelConventionsTable
    }

    /**
     * This creates that header you see at the top of the table with the plus minus, etc
     */
    private fun createMappingsTableDecorator(): JComponent {
        val panelForTable = ToolbarDecorator.createDecorator(labelConventionsTable, null)
            .setAddActionUpdater { e: AnActionEvent? -> true }
            .setAddAction { button: AnActionButton? ->
                createAddLabelConventionDialog()
            }
            .setRemoveActionUpdater { e: AnActionEvent? -> labelConventionsTable.selection.isNotEmpty() }
            .setRemoveAction { button: AnActionButton? ->
                labelConventionsTable.selectedObjects.forEach { unsavedSettings.currentScheme.labelConventions.remove(it) }
                loadScheme(unsavedSettings.currentScheme)
            }
            // Up/down actions currently have no function, other than allowing users to group them together in the UI
            .setMoveUpActionUpdater { e: AnActionEvent? -> labelConventionsTable.selectedRow > 0 }
            .setMoveUpAction {
                // take the list element above the selection block and move it to the end of the block
                val startRow = labelConventionsTable.selectedRow
                val endRow = labelConventionsTable.selectedRowCount + startRow - 1
                val holder = unsavedSettings.currentScheme.labelConventions[startRow - 1]

                unsavedSettings.currentScheme.labelConventions.removeAt(startRow - 1)
                unsavedSettings.currentScheme.labelConventions.add(endRow, holder)

                loadScheme(unsavedSettings.currentScheme)
                labelConventionsTable.setRowSelectionInterval(startRow - 1, endRow - 1)
            }
            .setMoveDownActionUpdater { e: AnActionEvent? -> labelConventionsTable.selectedRow < labelConventionsTable.rowCount - 1 }
            .setMoveDownAction {
                // take the list element below the selection block and move it to the start of the block
                val startRow = labelConventionsTable.selectedRow
                val endRow = labelConventionsTable.selectedRowCount + startRow - 1
                val holder = unsavedSettings.currentScheme.labelConventions[endRow + 1]

                unsavedSettings.currentScheme.labelConventions.removeAt(endRow + 1)
                unsavedSettings.currentScheme.labelConventions.add(startRow, holder)

                loadScheme(unsavedSettings.currentScheme)
                labelConventionsTable.setRowSelectionInterval(startRow + 1, endRow + 1)
            }
            .createPanel()
        panelForTable.preferredSize = JBDimension(-1, 200)
        return panelForTable
    }

    /**
     * In order to make it easier for the user to enter new values, we use a dialog before adding the entry to the list (might be out of view if the list is long).
     */
    private fun createAddLabelConventionDialog() {
        val typeField = ComboBox(LabelConventionType.values())
        val elementField = JBTextField().apply { preferredWidth = 200 }
        val prefixField = JBTextField().apply { preferredWidth = 200 }
        val labelField = JBCheckBox()

        DialogBuilder().apply {
            setCenterPanel(
                panel {
                    row("Type:") { cell(typeField) }
                    row("Environment/command name:") { cell(elementField) }
                    row("Prefix:") { cell(prefixField) }
                    row("Should have label:") { cell(labelField) }
                }
            )

            addOkAction()
            addCancelAction()
            title("Add Label Convention")

            if (show() == DialogWrapper.OK_EXIT_CODE) {
                val newLabel = LabelConvention(
                    labelField.isSelected,
                    typeField.selectedItem as LabelConventionType,
                    elementField.text,
                    prefixField.text
                )
                unsavedSettings.currentScheme.labelConventions.add(newLabel)
                loadScheme(unsavedSettings.currentScheme)
                labelConventionsTable.setRowSelectionInterval(
                    labelConventionsTable.rowCount - 1,
                    labelConventionsTable.rowCount - 1
                )
            }
        }
    }

    private fun buildEnvironmentSelector(): ComboBox<LabelConventionType> {
        return ComboBox(LabelConventionType.values())
    }

    override fun createComponent(): JComponent {
        schemesPanel = TexifyConventionsSchemesPanel(unsavedSettings)

        /**
         * Save the current scheme and load the new scheme whenever the combobox changes
         */
        schemesPanel.addListener(object : TexifyConventionsSchemesPanel.Listener {
            override fun onCurrentSchemeWillChange(scheme: TexifyConventionsScheme) {
                saveScheme(scheme)
            }

            override fun onCurrentSchemeHasChanged(scheme: TexifyConventionsScheme) {
                loadScheme(scheme)
            }
        })

        labelConventionsTable = createConventionsTable()

        createMappingsTableDecorator()

        labelCommandType = buildEnvironmentSelector()
        labelCommandType.addItemListener { e: ItemEvent? ->
            if (labelConventionsTable.isEditing) {
                labelConventionsTable.stopEditing()
            }
        }

        val centerPanel = panel {
            row {
                label("Maximum section size (characters)")
                spinner(1..Integer.MAX_VALUE, 500).apply {
                    maxSectionSize = component
                    component.number = 4000
                }
            }

            group("Labels") {
                row {
                    cell(
                        createMappingsTableDecorator()
                    )
                        .resizableColumn()
                        .gap(RightGap.SMALL)
                        .align(AlignX.FILL)
                }
            }
        }

        mainPanel = JPanel(BorderLayout())
        mainPanel.add(schemesPanel, BorderLayout.NORTH)
        mainPanel.add(centerPanel, BorderLayout.CENTER)
        loadSettings(settingsManager.getSettings())
        return mainPanel
    }

    /**
     * Initialize the view elements with the settings from the supplied scheme
     */
    private fun loadScheme(scheme: TexifyConventionsScheme) {
        maxSectionSize.value = scheme.maxSectionSize

        // make sure to make a copy so changes to the elements are transferred explicitly
        val items = scheme.labelConventions.map { l -> l.copy() }
        val model = ListTableModel(arrayOf(typeColumnInfo, nameColumnInfo, prefixColumnInfo, enabledColumnInfo), items)
        labelConventionsTable.model = model
    }

    /**
     * Transfer the view elements into the supplied scheme
     */
    private fun saveScheme(scheme: TexifyConventionsScheme) {
        scheme.maxSectionSize = maxSectionSize.number
        scheme.labelConventions.clear()
        scheme.labelConventions.addAll(labelConventionsTable.items)
    }

    /**
     * Load the supplied settings and update the view component to reflect the setting values
     */
    private fun loadSettings(settings: TexifyConventionsSettings) {
        unsavedSettings.copyFrom(settings)
        loadScheme(unsavedSettings.currentScheme)
        schemesPanel.updateComboBoxList()
    }

    override fun isModified(): Boolean {
        saveScheme(unsavedSettings.currentScheme)
        return settingsManager.getSettings() != unsavedSettings
    }

    override fun reset() = loadSettings(settingsManager.getSettings())

    /**
     * Persist the currently active settings
     */
    override fun apply() {
        saveScheme(unsavedSettings.currentScheme)
        settingsManager.saveSettings(unsavedSettings)
    }

    override fun getDisplayName() = "Conventions"

    override fun getId() = "TexifyConventionsConfigurable"

    override fun isProjectLevel(): Boolean =
        ::schemesPanel.isInitialized && schemesPanel.model.settings.currentScheme.isProjectScheme
}

package nl.hannahsten.texifyidea.settings.conventions

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage

/**
 * Settings manager that persists and loads settings from a global settings file.
 *
 * The class is internal because clients should use the [TexifyConventionsSettingsManager] facade.
 */
@State(name = "Conventions", storages = [Storage("texifySettings.xml")])
internal class TexifyConventionsGlobalSettingsManager : PersistentStateComponent<TexifyConventionsGlobalState> {

    private var globalState: TexifyConventionsGlobalState = TexifyConventionsGlobalState()

    override fun getState(): TexifyConventionsGlobalState = globalState

    override fun loadState(newState: TexifyConventionsGlobalState) {
        if (newState.schemes.any { it.isProjectScheme })
            throw IllegalStateException("GlobalSettingsManager cannot save project schemes")
        globalState = newState
    }
}

package nl.hannahsten.texifyidea.settings.conventions

/**
 * Global convention settings.
 *
 * The global settings store a list of schemes, one of which is the global default scheme. In addition, the global
 * settings remember which scheme is currently selected. Instances of this class must be serializable, since they are
 * persisted by a [TexifyConventionsGlobalSettingsManager]. Changing the properties to "val" silently fails
 * serialization.
 *
 * The class is internal because clients should only use the [TexifyConventionsSettings] abstraction.
 */
internal data class TexifyConventionsGlobalState(
    var selectedScheme: String = TexifyConventionsScheme.DEFAULT_SCHEME_NAME,
    var schemes: List<TexifyConventionsScheme> = listOf(TexifyConventionsScheme())
) {

    fun deepCopy() = copy(schemes = schemes.map { it.copy() })
}

package nl.hannahsten.texifyidea.settings.conventions

import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.project.Project

/**
 * Settings manager that persists and loads settings from a project local settings file (i.e., below the .idea folder).
 *
 * The class is internal because clients should use the [TexifyConventionsSettingsManager] facade.
 */
@State(name = "Conventions", storages = [Storage("texifySettings.xml")])
internal class TexifyConventionsProjectSettingsManager(var project: Project? = null) :
    PersistentStateComponent<TexifyConventionsProjectState> {

    private var projectState: TexifyConventionsProjectState = TexifyConventionsProjectState()

    override fun getState(): TexifyConventionsProjectState = projectState

    override fun loadState(newState: TexifyConventionsProjectState) {
        if (!newState.scheme.isProjectScheme)
            throw IllegalStateException("ProjectSettingsManager can only save project schemes")
        projectState = newState
    }
}

package nl.hannahsten.texifyidea.settings.conventions

/**
 * Per project convention settings.
 *
 * The per-project settings store a single project scheme and a flag that indicates whether the project scheme or the
 * global scheme should be used. Instances of this class must be serializable, since they are persisted by a
 * [TexifyConventionsProjectSettingsManager]. Changing the properties to "val" silently fails serialization.
 *
 * The class is internal because clients should only use the [TexifyConventionsSettings] abstraction.
 */
internal data class TexifyConventionsProjectState(
    var scheme: TexifyConventionsScheme = TexifyConventionsScheme(
        myName = TexifyConventionsScheme.PROJECT_SCHEME_NAME,
    ),
    var useProjectScheme: Boolean = false
) {

    fun deepCopy() = copy(scheme = scheme.deepCopy())
}

package nl.hannahsten.texifyidea.settings.conventions

import nl.hannahsten.texifyidea.lang.DefaultEnvironment.*
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexListingCommand.LSTINPUTLISTING
import nl.hannahsten.texifyidea.util.magic.env

/**
 * A scheme instance for storing settings regarding Texify conventions. Default values of this class represent the
 * default settings
 *
 * Scheme instances generally store settings for a specific scope. Concrete scheme implementations decide on which
 * scopes make sense. For example, a scheme could represent different color settings for code or different inspection
 * rules. In the context of Texify conventions, a scheme stores either project or IDE level conventions.
 *
 * Instances must be serializable since they are persisted as part of [TexifyConventionsGlobalState] or
 * [TexifyConventionsProjectState]. Changing the properties to "val" silently fails serialization.
 */
data class TexifyConventionsScheme(

    /**
     * The name of the scheme
     */
    var myName: String = DEFAULT_SCHEME_NAME,

    /**
     * The maximum section size before the corresponding inspection issues a warning.
     */
    var maxSectionSize: Int = 4000,

    /**
     * List of configured conventions.
     * The default conventions mirror the settings currently read from *Magic classes and the existing settings UI.
     * For example, which label conventions are enabled by default corresponds to the minimum section level which
     * should receive a label, but provides more fine-grained control.
     */
    var labelConventions: MutableList<LabelConvention> = mutableListOf(
        LabelConvention(true, LabelConventionType.COMMAND, PART.command, "part"),
        LabelConvention(true, LabelConventionType.COMMAND, CHAPTER.command, "ch"),
        LabelConvention(true, LabelConventionType.COMMAND, SECTION.command, "sec"),
        LabelConvention(true, LabelConventionType.COMMAND, SUBSECTION.command, "subsec"),
        LabelConvention(false, LabelConventionType.COMMAND, SUBSUBSECTION.command, "subsubsec"),
        LabelConvention(false, LabelConventionType.COMMAND, PARAGRAPH.command, "par"),
        LabelConvention(false, LabelConventionType.COMMAND, SUBPARAGRAPH.command, "subpar"),
        LabelConvention(false, LabelConventionType.COMMAND, ITEM.command, "itm"),
        LabelConvention(true, LabelConventionType.COMMAND, LSTINPUTLISTING.command, "lst"),
        LabelConvention(true, LabelConventionType.ENVIRONMENT, FIGURE.env, "fig"),
        LabelConvention(true, LabelConventionType.ENVIRONMENT, TABLE.env, "tab"),
        LabelConvention(true, LabelConventionType.ENVIRONMENT, EQUATION.env, "eq"),
        LabelConvention(true, LabelConventionType.ENVIRONMENT, ALGORITHM.env, "alg"),
        LabelConvention(true, LabelConventionType.ENVIRONMENT, LISTINGS.env, "lst"),
        LabelConvention(true, LabelConventionType.ENVIRONMENT, VERBATIM_CAPITAL.env, "verb"),
    )
) : com.intellij.openapi.options.Scheme {

    val isProjectScheme: Boolean
        get() = name == PROJECT_SCHEME_NAME

    companion object {

        const val DEFAULT_SCHEME_NAME = "Default"
        const val PROJECT_SCHEME_NAME = "Project"
    }

    fun deepCopy(): TexifyConventionsScheme =
        copy(labelConventions = labelConventions.map { it.copy() }.toMutableList())

    /**
     * Same as [myName].
     */
    override fun getName() = myName

    /**
     * Copy all settings except the name from the given scheme. This method is useful when you want to transfer settings
     * from one instance to another.
     */
    fun copyFrom(scheme: TexifyConventionsScheme) {
        if (scheme != this) {
            maxSectionSize = scheme.maxSectionSize
            labelConventions.clear()
            labelConventions.addAll(scheme.labelConventions)
        }
    }
}

package nl.hannahsten.texifyidea.settings.conventions

import com.intellij.application.options.schemes.AbstractSchemeActions
import com.intellij.application.options.schemes.SchemesModel
import com.intellij.application.options.schemes.SimpleSchemesPanel
import javax.naming.OperationNotSupportedException

/**
 *  Controller for managing the convention settings. The panel allows selecting and editing the current scheme and copying settings
 *  between the schemes. The supplied [TexifyConventionsSettings] instance represents the corresponding model,
 *  containing the available schemes.
 *
 *  @see TexifyConventionsScheme
 *  @see TexifyConventionsSettings
 */
internal class TexifyConventionsSchemesPanel(val settings: TexifyConventionsSettings) :
    SimpleSchemesPanel<TexifyConventionsScheme>(),
    SchemesModel<TexifyConventionsScheme> {

    val type: Class<TexifyConventionsScheme>
        get() = TexifyConventionsScheme::class.java

    /**
     * Listeners for changes in the scheme selection.
     */
    private val listeners = mutableListOf<Listener>()

    /**
     * Actions that can be performed with this panel.
     */
    val actions = createSchemeActions()

    /**
     * Registers a listener that will be informed whenever the scheme selection changed.
     *
     * @param listener the listener that listens to scheme-change events
     */
    fun addListener(listener: Listener) = listeners.add(listener)

    /**
     * Forcefully updates the combo box so that its entries and the current selection reflect the `settings` instance.
     *
     * This panel instance will update the combo box by itself. Call this method if the `settings` instance has been
     * changed externally and these changes need to be reflected.
     */
    fun updateComboBoxList() {
        settings.currentScheme.also { currentScheme ->
            resetSchemes(settings.schemes)
            selectScheme(currentScheme)
        }
    }

    /**
     * Returns true if a scheme with the given name is present in this panel.
     *
     * @param name the name to check for
     * @param projectScheme ignored
     * @return true if a scheme with the given name is present in this panel
     */
    override fun containsScheme(name: String, projectScheme: Boolean) = settings.schemes.any { it.name == name }

    /**
     * Returns an object with a number of actions that can be performed on this panel.
     *
     * @return an object with a number of actions that can be performed on this panel
     */
    override fun createSchemeActions() = SchemeActions()

    override fun getModel() = this

    override fun supportsProjectSchemes() = true

    override fun highlightNonDefaultSchemes() = true

    override fun useBoldForNonRemovableSchemes() = true

    override fun isProjectScheme(scheme: TexifyConventionsScheme) = scheme.isProjectScheme

    override fun canDeleteScheme(scheme: TexifyConventionsScheme) = false

    override fun canDuplicateScheme(scheme: TexifyConventionsScheme) = false

    override fun canRenameScheme(scheme: TexifyConventionsScheme) = false

    override fun canResetScheme(scheme: TexifyConventionsScheme) = true

    override fun differsFromDefault(scheme: TexifyConventionsScheme): Boolean {
        // schemes differ if any setting except the name is different
        return scheme.deepCopy() != TexifyConventionsScheme(myName = scheme.myName)
    }

    override fun removeScheme(scheme: TexifyConventionsScheme) {
        throw OperationNotSupportedException()
    }

    /**
     * The actions that can be performed with this panel.
     */
    inner class SchemeActions : AbstractSchemeActions<TexifyConventionsScheme>(this) {

        /**
         * Called when the user changes the scheme using the combo box.
         *
         * @param scheme the scheme that has become the selected scheme
         */
        public override fun onSchemeChanged(scheme: TexifyConventionsScheme?) {
            if (scheme == null)
                return

            listeners.forEach { it.onCurrentSchemeWillChange(settings.currentScheme) }
            settings.currentScheme = scheme
            listeners.forEach { it.onCurrentSchemeHasChanged(scheme) }
        }

        override fun copyToIDE(scheme: TexifyConventionsScheme) {
            settings.copyToDefaultScheme(scheme)
        }

        override fun copyToProject(scheme: TexifyConventionsScheme) {
            settings.copyToProjectScheme(scheme)
        }

        override fun getSchemeType() = type

        override fun resetScheme(scheme: TexifyConventionsScheme) {
            scheme.copyFrom(TexifyConventionsScheme())
            listeners.forEach { it.onCurrentSchemeHasChanged(scheme) }
        }

        /**
         * Duplicates the currently active scheme.
         *
         * This method is useful only if there can be other schemes besides the projet and the global default scheme,
         * which is currently not supported.
         */
        override fun duplicateScheme(scheme: TexifyConventionsScheme, newName: String) {
            throw OperationNotSupportedException()
        }

        /**
         * Renames the currently active scheme.
         *
         * This method is useful only if there can be other schemes besides the projet and the global default scheme,
         * which is currently not supported.
         */

        override fun renameScheme(scheme: TexifyConventionsScheme, newName: String) {
            throw OperationNotSupportedException()
        }
    }

    /**
     * A listener that listens to events that occur to this panel.
     */
    interface Listener {

        /**
         * Invoked when the currently-selected scheme is about to change.
         *
         * @param scheme the scheme that is about to be replaced in favor of another scheme
         */
        fun onCurrentSchemeWillChange(scheme: TexifyConventionsScheme)

        /**
         * Invoked when the currently-selected scheme has just been changed.
         *
         * @param scheme the scheme that has become the currently-selected scheme
         */
        fun onCurrentSchemeHasChanged(scheme: TexifyConventionsScheme)
    }
}

package nl.hannahsten.texifyidea.settings.conventions

/**
 * Model for the Texify convention settings.
 *
 * A model consists of a project state and a global state and presents a unified view on both. For example, modifying
 * the currently active scheme modifies either the project state or the global state, depending on which scheme is
 * selected.
 */
data class TexifyConventionsSettings internal constructor(
    private var projectState: TexifyConventionsProjectState = TexifyConventionsProjectState(
        TexifyConventionsScheme(myName = TexifyConventionsScheme.PROJECT_SCHEME_NAME)
    ),
    private var globalState: TexifyConventionsGlobalState = TexifyConventionsGlobalState()
) {

    /**
     * Copies the settings of the supplied scheme to the default (global) scheme
     *
     * The method is internal because it should only be called by the convention settings GUI components
     */
    internal fun copyToDefaultScheme(scheme: TexifyConventionsScheme) {
        globalState.schemes.single { it.name == TexifyConventionsScheme.DEFAULT_SCHEME_NAME }.copyFrom(scheme)
    }

    /**
     * Copies the settings of the supplied scheme to the project scheme
     *
     * The method is internal because it should only be called by the convention settings GUI components
     */
    internal fun copyToProjectScheme(scheme: TexifyConventionsScheme) {
        projectState.scheme.copyFrom(scheme)
    }

    /**
     * Copies the settings from another settings instance
     *
     * The method is internal because it should only be called by the convention settings GUI components
     */
    internal fun copyFrom(newState: TexifyConventionsSettings) {
        projectState = newState.projectState.deepCopy()
        globalState = newState.globalState.deepCopy()
    }

    /**
     * Returns the substates constituting this settings instance
     *
     * The method is internal because it should only be called by the [TexifyConventionsSettingsManager] for persistence
     */
    internal fun getStateCopy() = Pair(projectState.deepCopy(), globalState.deepCopy())

    /**
     * The currently active scheme.
     *
     * When changing the scheme make sure that the new scheme is either a project scheme or a known global scheme.
     */
    var currentScheme: TexifyConventionsScheme
        get() {
            return if (projectState.useProjectScheme) projectState.scheme
            else schemes.firstOrNull { it.name == globalState.selectedScheme }
                ?: throw IllegalStateException("No scheme named ${globalState.selectedScheme} exists")
        }
        set(scheme) {
            if (scheme.isProjectScheme) {
                projectState.scheme.copyFrom(scheme)
                projectState.useProjectScheme = true
            }
            else {
                val globalScheme = globalState.schemes.singleOrNull { it.name == scheme.name }
                    ?: throw IllegalArgumentException("Scheme ${scheme.name} is neither a project scheme nor a known global scheme")
                globalScheme.copyFrom(scheme)
                globalState.selectedScheme = scheme.name
                projectState.useProjectScheme = false
            }
        }

    val schemes: List<TexifyConventionsScheme>
        get() = listOfNotNull(*globalState.schemes.toTypedArray(), projectState.scheme)

    /**
     * Convenience method to retrieve the currently active label convention
     *
     * @param name the name of the command or environment of the label convention
     */
    fun getLabelConvention(name: String?, type: LabelConventionType): LabelConvention? {
        val conventionName = if (type == LabelConventionType.COMMAND) {
            name?.replace("\\", "")
        }
        else {
            name
        }

        return currentScheme.labelConventions.singleOrNull { c -> c.name == conventionName && c.type == type }
    }

    fun getDefaultLabel(): LabelConvention {
        return LabelConvention(false, LabelConventionType.ENVIRONMENT, "new-environment", "")
    }

    fun deleteProjectLabel(index: Int) {
        projectState.scheme.labelConventions.removeAt(index)
    }

    fun deleteProjectLabel(item: LabelConvention) {
        projectState.scheme.labelConventions.remove(item)
    }
}

package nl.hannahsten.texifyidea.settings.conventions

import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project

/**
 * A facade for the [TexifyConventionsProjectSettingsManager] and the [TexifyConventionsGlobalSettingsManager].
 * The class provides a convenient way for the settings UI, inspections or other Texify classes to query the currently
 * active conventions.
 *
 * This class is responsible for delegating the persistence of project and global parts of a [TexifyConventionsSettings]
 * to the appropriate settings manager. Conversely, the class constructs a new [TexifyConventionsSettings] object from
 * project and global state.
 */
class TexifyConventionsSettingsManager private constructor(
    private var projectSettings: TexifyConventionsProjectSettingsManager,
    private var globalSettings: TexifyConventionsGlobalSettingsManager
) {

    fun getSettings() = TexifyConventionsSettings(projectSettings.state, globalSettings.state)

    fun saveSettings(newState: TexifyConventionsSettings) {
        val state = newState.getStateCopy()
        projectSettings.loadState(state.first)
        globalSettings.loadState(state.second)
    }

    companion object {

        fun getInstance(project: Project) =
            TexifyConventionsSettingsManager(
                project.getService(TexifyConventionsProjectSettingsManager::class.java),
                service()
            )
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.projectRoots.*
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.Consumer
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.util.runCommand
import org.jdom.Element
import javax.swing.JComponent

/**
 * Currently, we only support MiKTeX Docker images, but it wouldn't be too difficult to extend for other images.
 */
class DockerSdk : LatexSdk("LaTeX Docker SDK") {

    companion object {

        val isAvailable: Boolean by lazy {
            getAvailableImages().any { it.contains("miktex") }
        }

        fun getAvailableImages(): List<String> =
            runCommand("docker", "image", "ls", "--format", "table {{.Repository}}:{{.Tag}}")?.split('\n')
                ?.drop(1) // header
                ?.filter { it.isNotBlank() } ?: emptyList()
    }

    override fun suggestHomePath(): String {
        // Path to Docker executable
        return "/usr/bin"
    }

    override fun suggestHomePaths(): MutableCollection<String> {
        // Windows is not supported for now
        if (!SystemInfo.isLinux) return mutableListOf()

        // There's not really a path with 'sources' for docker except the location of images, but that's OS-dependent
        // and we only need to be able to execute the 'docker' executable anyway
        return "which docker".runCommand()?.let { output ->
            mutableListOf(
                output.split("/").dropLast(1).joinToString("/")
            )
        } ?: mutableListOf()
    }

    override fun isValidSdkHome(path: String): Boolean {
        // For now we only support miktex images
        return "$path/docker image ls".runCommand()?.contains("miktex") ?: false
    }

    override fun getLatexDistributionType() = LatexDistributionType.DOCKER_MIKTEX

    override fun getVersionString(sdk: Sdk): String? {
        val data = sdk.sdkAdditionalData as? DockerSdkAdditionalData
        // Throw away the (possibly long) 'prefix'
        return data?.imageName?.split("/")?.lastOrNull()
    }

    override fun getExecutableName(executable: String, homePath: String): String {
        // Could be improved by prefixing docker command here, but needs to be in sync with LatexCompiler
        return executable
    }

    override fun getHomeChooserDescriptor(): FileChooserDescriptor {
        val descriptor = super.getHomeChooserDescriptor()
        descriptor.title = "Select the Directory Containing the Docker Executable"
        return descriptor
    }

    override fun getHomeFieldLabel() = "Path to directory containing Docker executable:"

    override fun getInvalidHomeMessage(path: String): String {
        return "Could not find docker executable $path/docker"
    }

    override fun createAdditionalDataConfigurable(sdkModel: SdkModel, sdkModificator: SdkModificator): AdditionalDataConfigurable {
        return DockerSdkConfigurable()
    }

    override fun saveAdditionalData(additionalData: SdkAdditionalData, additional: Element) {
        if (additionalData is DockerSdkAdditionalData) {
            additionalData.save(additional)
        }
    }

    override fun loadAdditionalData(additional: Element): SdkAdditionalData {
        return DockerSdkAdditionalData(additional)
    }

    override fun supportsCustomCreateUI() = true

    override fun showCustomCreateUI(
        sdkModel: SdkModel,
        parentComponent: JComponent,
        selectedSdk: Sdk?,
        sdkCreatedCallback: Consumer<in Sdk>
    ) {
        val chooseImageComponent = DockerSdkConfigurable().createComponent()
        val imagesComboBox = chooseImageComponent.components.filterIsInstance<ComboBox<String>>().firstOrNull()
        val dialog = DialogBuilder().apply {
            setTitle("Choose Docker Image")
            setCenterPanel(chooseImageComponent)
        }
        dialog.show()

        // Currently, we don't ask the user for this. See SdkConfigurationUtil.selectSdkHome
        val homePath = suggestHomePath()

        val sdk = SdkConfigurationUtil.createSdk(sdkModel.sdks.toMutableList(), homePath, this, DockerSdkAdditionalData(imagesComboBox?.selectedItem as? String), name)
        sdkCreatedCallback.consume(sdk)
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.projectRoots.SdkModel
import com.intellij.openapi.projectRoots.ValidatableSdkAdditionalData
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import org.jdom.Element

/**
 * Additional data to be saved with the [DockerSdk].
 *
 * @author Thomas
 */
class DockerSdkAdditionalData(val imageName: String?) : ValidatableSdkAdditionalData {

    companion object {

        private const val IMAGE = "image"
    }

    constructor(element: Element) : this(element.getAttributeValue(IMAGE))

    override fun checkValid(sdkModel: SdkModel?) {
        // For some reason this method is called on the initial additional data as well, and if it's not valid then the user cannot change it. So, we provide null as default value and then make null a valid name.
        if (imageName != null) {
            if (runCommandWithExitCode("docker", "image", "inspect", imageName).second != 0) {
                throw ConfigurationException("Cannot find docker image $imageName")
            }
        }
    }

    fun save(element: Element) {
        if (imageName != null) {
            element.setAttribute(IMAGE, imageName)
        }
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.projectRoots.AdditionalDataConfigurable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.components.JBLabel
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import java.awt.Dimension
import java.awt.FlowLayout
import javax.swing.JComponent
import javax.swing.JPanel

/**
 * Customized UI for the SDK settings page of [DockerSdk].
 *
 * @author Thomas
 */
class DockerSdkConfigurable : AdditionalDataConfigurable {

    private var sdk: Sdk? = null
    override fun setSdk(sdk: Sdk?) {
        this.sdk = sdk
    }

    private lateinit var imageName: ComboBox<String>

    override fun createComponent(): JComponent {
        val images = DockerSdk.getAvailableImages()
        imageName = ComboBox(images.toTypedArray())

        if (images.isEmpty()) {
            // Check if Docker is up and running
            val (output, exitCode) = runCommandWithExitCode("docker", "image", "ls", returnExceptionMessage = true)
            if (exitCode != 0) {
                return JBLabel("Error: $output")
            }
        }

        val selected = (sdk?.sdkAdditionalData as? DockerSdkAdditionalData)?.imageName
        if (selected != null) {
            imageName.selectedItem = selected
        }
        else if (imageName.itemCount > 0) {
            imageName.selectedIndex = 0
        }
        else {
            imageName.selectedIndex = -1
        }

        // Set width
        val longestString = images.maxByOrNull { it.length } ?: "miktex:latest"
        val width = imageName.getFontMetrics(imageName.font)
            .stringWidth(longestString)
        imageName.preferredSize = Dimension(width, imageName.preferredSize.height)

        return JPanel(FlowLayout(FlowLayout.LEFT)).apply {
            add(JBLabel("Docker image name: "))
            add(imageName)
        }
    }

    override fun isModified(): Boolean {
        val data = sdk?.sdkAdditionalData as? DockerSdkAdditionalData
        return data?.imageName != this.imageName.selectedItem as? String
    }

    override fun apply() {
        if (sdk == null) return
        val newData = DockerSdkAdditionalData(this.imageName.selectedItem as String)
        val modificator = sdk!!.sdkModificator
        modificator.versionString = newData.imageName
        modificator.sdkAdditionalData = newData
        ApplicationManager.getApplication().runWriteAction { modificator.commitChanges() }
    }

    override fun reset() {
        if (sdk == null) return
        val data = sdk!!.sdkAdditionalData as? DockerSdkAdditionalData ?: return
        imageName.selectedItem = data.imageName ?: return
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.codeInsight.daemon.ProjectSdkSetupValidator
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.SdkTypeId
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.roots.ui.configuration.SdkPopupFactory
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.ui.EditorNotificationPanel
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.grammar.LatexLanguage

/**
 * https://jetbrains.org/intellij/sdk/docs/reference_guide/project_model/sdk.html#assisting-in-setting-up-an-sdk
 *
 * @author Thomas
 */
class LatexProjectSdkSetupValidator : ProjectSdkSetupValidator {

    override fun isApplicableFor(project: Project, file: VirtualFile): Boolean {
        // Check if setting up a LaTeX SDK would make sense
        return file.fileType is LatexFileType || PsiManager.getInstance(project).findFile(file)?.language?.isKindOf(LatexLanguage) == true
    }

    override fun getErrorMessage(project: Project, file: VirtualFile): String? {
        // Based on https://github.com/rikvdkleij/intellij-haskell/blob/895a214b174b69f661d4f7d4230633058fca8f1e/src/main/scala/intellij/haskell/notification/HaskellProjectSdkSetupValidator.scala#L24
        // Nothing to do if we don't need an SDK
        if (LatexSdkUtil.isPdflatexInPath) return null
        val module = ModuleUtilCore.findModuleForFile(file, project) ?: return null
        if (ModuleRootManager.getInstance(module).sdk?.sdkType is LatexSdk || ProjectRootManager.getInstance(project).projectSdk?.sdkType is LatexSdk) return null
        return "No LaTeX installation could be found. Please add it to PATH or set up a LaTeX SDK (and reopen this file)."
    }

    override fun getFixHandler(project: Project, file: VirtualFile): EditorNotificationPanel.ActionHandler {
        return SdkPopupFactory.newBuilder().withProject(project)
            .withSdkTypeFilter { id: SdkTypeId -> id is LatexSdk }
            .updateProjectSdkFromSelection()
            .buildEditorNotificationPanelHandler()
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.projectRoots.*
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import org.jdom.Element

/**
 * Represents the location of the LaTeX installation.
 *
 * NOTES for subclasses:
 * - suggestHomePath() will be the starting point when someone opens the file explorer dialog to select an SDK of this type
 * - suggestHomePaths() appear under "Detected SDK's" when adding an SDK
 * - HOWEVER they only do so, when getVersionString(sdkHome: String?) is implemented (implementing getVersionString(sdk: SDK?) is NOT enough)
 *
 * @author Thomas
 */
abstract class LatexSdk(name: String) : SdkType(name) {

    override fun saveAdditionalData(additionalData: SdkAdditionalData, additional: Element) {}

    override fun createAdditionalDataConfigurable(sdkModel: SdkModel, sdkModificator: SdkModificator): AdditionalDataConfigurable? {
        return null
    }

    override fun suggestSdkName(currentSdkName: String?, sdkHome: String) = name

    override fun getPresentableName() = name

    override fun setupSdkPaths(sdk: Sdk) {
        val modificator = sdk.sdkModificator
        modificator.versionString = getVersionString(sdk)
        modificator.commitChanges() // save
    }

    /**
     * Interface between this and [LatexDistributionType], which is used in the run configuration.
     */
    abstract fun getLatexDistributionType(): LatexDistributionType

    /**
     * Construct a valid path to the executable, given the homepath.
     * This is different per SDK type, so therefore it is a method here.
     * Use [LatexSdkUtil.getExecutableName] if you don't know yet which SDK type should be used.
     */
    internal abstract fun getExecutableName(executable: String, homePath: String): String

    /**
     * Because we cannot add a Sources Path to the SDK programmatically, even though we know (given the SDK type)
     * where the sources path is, we implement a default on the SDK type (which will be invisible for the user unfortunately).
     */
    open fun getDefaultSourcesPath(homePath: String): VirtualFile? = null

    /**
     * Default path to the location of package style (.sty) files.
     * Example: texlive/2020/texmf-dist/tex/latex
     */
    open fun getDefaultStyleFilesPath(homePath: String): VirtualFile? = null
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.util.getLatexRunConfigurations
import nl.hannahsten.texifyidea.util.runCommand
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import java.io.File

/**
 * Utility functions which are not specific to a [LatexSdk] or a [nl.hannahsten.texifyidea.run.compiler.LatexCompiler].
 */
object LatexSdkUtil {

    val isPdflatexInPath: Boolean by lazy {
        "pdflatex --version".runCommand()?.contains("pdfTeX") == true
    }

    val pdflatexVersionText: String by lazy {
        getDistribution()
    }

    /**
     * Whether the user is using MikTeX or not.
     * This value is lazy, so only computed when first accessed, because it is unlikely that the user will change LaTeX distribution while using IntelliJ.
     */
    val isMiktexAvailable: Boolean by lazy {
        pdflatexVersionText.contains("MiKTeX")
    }

    private val isWslTexliveAvailable: Boolean by lazy {
        SystemInfo.isWindows && runCommand("bash", "-ic", "pdflatex --version")?.contains("pdfTeX") == true
    }

    val isTlmgrInstalled: Boolean by lazy {
        "tlmgr --version".runCommand()?.contains("TeX Live") == true
    }

    // Cache for below function
    private var isTlgmrAvailable: Boolean? = null

    /**
     * Contrary to e.g. with pdflatex, we want to fail silently if tlmgr is not available.
     * Therefore we check if it really is available after getting the executable name/path.
     */
    fun isTlmgrAvailable(project: Project): Boolean {
        if (isTlmgrInstalled) {
            isTlgmrAvailable = true
            return true
        }
        isTlgmrAvailable?.let { return it }
        // If not set, find out if it is available
        val tlmgrExecutable = getExecutableName("tlmgr", project)
        isTlgmrAvailable = "$tlmgrExecutable --version".runCommand()?.contains("TeX Live") == true
        return isTlgmrAvailable!!
    }

    /**
     * Whether the user does not have MiKTeX or TeX Live, but does have the miktex docker image available.
     * In this case we assume the user wants to use Dockerized MiKTeX.
     */
    private fun defaultIsDockerMiktex() =
        (!isMiktexAvailable && !TexliveSdk.isAvailable && DockerSdk.isAvailable)

    @Suppress("RedundantIf")
    fun isAvailable(type: LatexDistributionType, project: Project): Boolean {
        if (type == LatexDistributionType.PROJECT_SDK && getLatexProjectSdk(project) != null) return true
        if (type == LatexDistributionType.MIKTEX && isMiktexAvailable) return true
        if (type == LatexDistributionType.TEXLIVE && TexliveSdk.isAvailable) return true
        if (type == LatexDistributionType.DOCKER_MIKTEX && DockerSdk.isAvailable) return true
        if (type == LatexDistributionType.WSL_TEXLIVE && isWslTexliveAvailable) return true
        return false
    }

    /**
     * Given the path to the LaTeX home, find the parent path of the executables, e.g. /bin/x86_64-linux/
     */
    fun getPdflatexParentPath(homePath: String) = File("$homePath/bin").listFiles()?.firstOrNull()?.path

    /**
     * Run pdflatex in the given directory and check if it is present and valid.
     */
    fun isPdflatexPresent(directory: String?): Boolean {
        // .exe is optional on windows
        val output = runCommandWithExitCode("$directory${File.separator}pdflatex", "--version", returnExceptionMessage = true).first ?: "No output given by $directory${File.separator}pdflatex --version"

        return output.contains("pdfTeX")
    }

    /**
     * Find the full name of the distribution in use, e.g. TeX Live 2019.
     */
    private fun getDistribution(): String {
        // Could be improved by using the (project-level) LaTeX SDK if pdflatex is not in PATH
        return parsePdflatexOutput(runCommand("pdflatex", "--version") ?: "")
    }

    /**
     * Parse the output of pdflatex --version and return the distribution.
     * Assumes the distribution name is in brackets at the end of the first line.
     */
    fun parsePdflatexOutput(output: String): String {
        val firstLine = output.split("\n")[0]
        val splitLine = firstLine.split("(", ")")

        // Get one-to-last entry, as the last one will be empty after the closing )
        return if (splitLine.size >= 2) {
            splitLine[splitLine.size - 2]
        }
        else {
            ""
        }
    }

    /**
     * Get default LaTeX distribution type (for the run configuration).
     */
    fun getDefaultLatexDistributionType(project: Project): LatexDistributionType {
        return when {
            getLatexProjectSdk(project) != null -> LatexDistributionType.PROJECT_SDK
            isMiktexAvailable -> LatexDistributionType.MIKTEX
            TexliveSdk.isAvailable -> LatexDistributionType.TEXLIVE
            defaultIsDockerMiktex() -> LatexDistributionType.DOCKER_MIKTEX
            else -> LatexDistributionType.TEXLIVE
        }
    }

    /**
     * Get executable name of pdflatex, which in case it is not in PATH may be prefixed by the full path (or even by a docker command).
     */
    fun getExecutableName(executableName: String, project: Project, latexDistributionType: LatexDistributionType? = null): String {
        // Prefixing the LaTeX compiler is not relevant for Docker MiKTeX (perhaps the path to the docker executable)
        if (latexDistributionType == LatexDistributionType.DOCKER_MIKTEX) return executableName

        // Give preference to the project SDK if a valid LaTeX SDK is selected
        getLatexProjectSdk(project)?.let { sdk ->
            if (sdk.homePath != null) {
                (sdk.sdkType as? LatexSdk)?.getExecutableName(executableName, sdk.homePath!!)?.let { return it }
            }
        }
        // If not, if it's in path then that also works
        if (isPdflatexInPath) {
            return executableName
        }

        // Maybe we're on a Mac but in a non-IntelliJ IDE, in which case the user provided the path to pdflatex in the run config (as it's not possible to configure an SDK)
        project.getLatexRunConfigurations().mapNotNull { it.compilerPath?.substringBefore("/pdflatex") }.forEach {
            val file = File(it, executableName)
            if (file.isFile) return file.path
        }

        // If it's also not in path, just try a few sdk types with the default home path
        val preferredSdk = getPreferredSdkType()?.sdkType as? LatexSdk ?: return executableName
        return preferredSdk.suggestHomePath()?.let { preferredSdk.getExecutableName(executableName, it) } ?: executableName
    }

    /**
     * Assuming the goal is to be able to execute e.g. pdflatex, try to find a suitable LaTeX distribution to find a command to use pdflatex.
     * Check all available Project SDKs and return a good one (arbitrary order of preference).
     */
    private fun getPreferredSdkType(): Sdk? {
        val allSdks = ProjectJdkTable.getInstance().allJdks.filter { it.sdkType is LatexSdk }
        allSdks.firstOrNull { it.sdkType is TexliveSdk }?.let { return it }
        allSdks.firstOrNull { it.sdkType is MiktexWindowsSdk }?.let { return it }
        allSdks.firstOrNull { it.sdkType is DockerSdk }?.let { return it }
        return null
    }

    /**
     * If a LaTeX SDK is selected as project SDK, return it, otherwise return null.
     */
    fun getLatexProjectSdk(project: Project): Sdk? {
        val sdk = ProjectRootManager.getInstance(project).projectSdk
        if (sdk?.sdkType is LatexSdk) {
            return sdk
        }
        return null
    }

    /**
     * Get type of project SDK. If null or not a LaTeX sdk, return null.
     */
    fun getLatexProjectSdkType(project: Project): LatexSdk? {
        return getLatexProjectSdk(project)?.sdkType as? LatexSdk
    }

    /**
     * Collect SDK source paths, so paths to texmf-dist/source/latex, based on Project SDK if available (combining the default
     * for the SDK type and any user-added source roots) and otherwise on a random guess (ok not really).
     *
     * @param getRoots Given an sdk type and a home path, return a list of source roots.
     */
    fun getSdkSourceRoots(project: Project, getRoots: (LatexSdk, String) -> VirtualFile?): Set<VirtualFile> {
        // Get user provided and default source roots
        getLatexProjectSdk(project)?.let { sdk ->
            if (sdk.sdkType !is LatexSdk) return@let
            val userProvided = sdk.rootProvider.getFiles(OrderRootType.SOURCES).toSet()
            val default = if (sdk.homePath != null) setOf(getRoots(sdk.sdkType as LatexSdk, sdk.homePath!!)).filterNotNull() else emptySet()
            return userProvided + default
        }

        // If no sdk is known, guess something
        for (sdkType in setOf(TexliveSdk(), NativeTexliveSdk(), MiktexWindowsSdk())) {
            val roots = sdkType.suggestHomePaths().mapNotNull { homePath -> getRoots(sdkType, homePath) }.toSet()
            if (roots.isNotEmpty()) return roots
        }
        return emptySet()
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.util.runCommand
import java.nio.file.Paths

/**
 * MiKTeX on Linux has a very different file structure than MiKTeX on Windows, so this warrants its own SDK type.
 * This has been tested on Arch Linux, and let's hope it's similar for other distros.
 * Based on user feedback, I'm guessing that it is very similar to MiKTeX for Mac.
 *
 * On Arch (user install), the pdflatex binary was created in ~/bin and MiKTeX itself was installed to /opt/miktex, the texmf folder was in ~/.miktex.
 */
class MiktexLinuxSdk : LatexSdk("MiKTeX Mac/Linux SDK") {

    companion object {

        // Cache version
        var version: String? = null
    }

    override fun getLatexDistributionType() = LatexDistributionType.MIKTEX

    override fun getExecutableName(executable: String, homePath: String): String {
        return "$homePath/$executable"
    }

    override fun suggestHomePath(): String {
        // The user can by default install globally or for the user only (see below)
        // Similar as with Windows we recommend installing for the user only.
        return "~/bin/"
    }

    override fun suggestHomePaths(): MutableCollection<String> {
        return listOf(
            Paths.get(System.getProperty("user.home"), "bin").toString(),
            "/usr/local/bin"
        ).toMutableList()
    }

    override fun isValidSdkHome(path: String): Boolean {
        // We just want a path where pdflatex is present
        return LatexSdkUtil.isPdflatexPresent(path)
    }

    override fun getInvalidHomeMessage(path: String) = "Could not find $path/pdflatex"

    override fun getVersionString(sdk: Sdk): String? {
        return getVersionString(sdk.homePath ?: return null)
    }

    override fun getVersionString(sdkHome: String): String? {
        version?.let { return version }

        val executable = getExecutableName("pdflatex", sdkHome)
        val output = "$executable --version".runCommand() ?: ""
        version = "\\(MiKTeX (\\d+.\\d+)\\)".toRegex().find(output)?.value

        return version
    }

    override fun getDefaultSourcesPath(homePath: String): VirtualFile? {
        // This was the path on the tested Arch installation
        // Note that also these files are zipped
        return LocalFileSystem.getInstance().findFileByPath(Paths.get(System.getProperty("user.home"), ".miktex", "texmfs", "install", "source").toString())
    }

    override fun getDefaultStyleFilesPath(homePath: String): VirtualFile? {
        return LocalFileSystem.getInstance().findFileByPath(Paths.get(System.getProperty("user.home"), ".miktex", "texmfs", "install", "tex", "latex").toString())
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.util.Log
import nl.hannahsten.texifyidea.util.runCommand
import nl.hannahsten.texifyidea.util.runCommandWithExitCode
import org.apache.maven.artifact.versioning.DefaultArtifactVersion
import java.nio.file.InvalidPathException
import java.nio.file.Paths

/**
 * MiKTeX on Windows.
 */
class MiktexWindowsSdk : LatexSdk("MiKTeX Windows SDK") {

    companion object {

        // Cache version
        var version: DefaultArtifactVersion? = null
    }

    override fun getLatexDistributionType() = LatexDistributionType.MIKTEX

    override fun getExecutableName(executable: String, homePath: String): String {
        val path = LatexSdkUtil.getPdflatexParentPath(Paths.get(homePath, "miktex").toString()) ?: return executable
        return Paths.get(path, executable).toString()
    }

    override fun suggestHomePath(): String {
        return Paths.get(System.getProperty("user.home"), "AppData", "Local", "Programs", "MiKTeX 2.9").toString()
    }

    override fun suggestHomePaths(): MutableCollection<String> {
        val results = mutableSetOf<String>()
        val (paths, exitCode) = runCommandWithExitCode("where", "pdflatex")
        if (paths != null && exitCode == 0) {
            paths.split("\r\n").forEach { path ->
                if (path.isBlank()) return@forEach
                val index = path.findLastAnyOf(setOf("miktex\\bin"))?.first ?: (path.length - 1)
                if (index > 0) {
                    results.add(path.substring(0, index))
                }
            }
        }
        else {
            results.add(suggestHomePath())
        }
        return results
    }

    override fun getDefaultSourcesPath(homePath: String): VirtualFile? {
        val path = Paths.get(homePath, "source").toString()
        return try {
            // To save space, MiKTeX leaves source/latex empty by default, but does leave the zipped files in source/
            LocalFileSystem.getInstance().findFileByPath(path)
        }
        catch (ignored: InvalidPathException) {
            Log.debug("Invalid path $path when looking for LaTeX sources")
            null
        }
    }

    override fun getDefaultStyleFilesPath(homePath: String): VirtualFile? {
        val path = Paths.get(homePath, "tex", "latex").toString()
        return try {
            LocalFileSystem.getInstance().findFileByPath(path)
        }
        catch (ignored: InvalidPathException) {
            Log.debug("Invalid path $path when looking for LaTeX style files")
            null
        }
    }

    override fun isValidSdkHome(path: String): Boolean {
        // We want the MiKTeX 2.9 folder to be selected
        // Assume path is of the form C:\\Users\\username\\AppData\\Local\\Programs\\MiKTeX 2.9\\miktex\\bin\\x64\\pdflatex.exe
        val directory = LatexSdkUtil.getPdflatexParentPath(Paths.get(path, "miktex").toString())
        return LatexSdkUtil.isPdflatexPresent(directory)
    }

    override fun getInvalidHomeMessage(path: String) = "Could not find $path/miktex/bin/*/pdflatex, please make sure you selected the MiKTeX installation directory."

    override fun getVersionString(sdk: Sdk): String? {
        return getVersionString(sdk.homePath ?: return null)
    }

    override fun getVersionString(sdkHome: String) = "MiKTeX " + getVersion(sdkHome).toString()

    fun getVersion(sdkHome: String?): DefaultArtifactVersion {
        version?.let { return it }
        val executable = sdkHome?.let { getExecutableName("pdflatex", it) } ?: "pdflatex"
        val output = "$executable --version".runCommand() ?: ""
        val versionString = "\\(MiKTeX (\\d+.\\d+)\\)".toRegex().find(output)?.groups?.get(1)?.value ?: ""
        version = DefaultArtifactVersion(versionString)
        return version!!
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.projectRoots.Sdk
import nl.hannahsten.texifyidea.util.containsAny
import nl.hannahsten.texifyidea.util.runCommand

/**
 * TeX Live, as installed natively by the OS's package manager.
 * Differences with [TexliveSdk] are for example that not all executables (like tlmgr) are available,
 * and the texmf-dist folder is not assumed to be in a fixed location relative to the executables, but can be anywhere.
 *
 * Documentation may or may not be present (on Arch it requires texlive-most-doc).
 * tlmgr may or may not work (on Arch it requires manual config or tllocalmgr from tllocalmgr-git).
 * Source files may or may not be present (on Arch they are present for texlive-full, but not for texlive-core).
 * All in all, it's a lot of fun.
 */
class NativeTexliveSdk : TexliveSdk("Native TeX Live SDK") {

    companion object {

        // Path to texmf-dist, e.g. /usr/share/texmf-dist/ for texlive-core on Arch or /opt/texlive/2020/texmf-dist/ for texlive-full
        val texmfDistPath: String by lazy {
            "kpsewhich article.sty".runCommand()?.substringBefore("texmf-dist") + "texmf-dist"
        }
    }

    override fun suggestHomePath(): String {
        // This method should work fast and allow running from the EDT thread.
        // It will be the starting point when someone opens the file explorer dialog to select an SDK of this type
        return "/usr/bin"
    }

    override fun suggestHomePaths(): MutableCollection<String> {
        return TexliveSdk().suggestHomePaths().plus(suggestHomePath())
            // Avoid duplicates of TexliveSdks, which probably have x86_64-linux in the path
            .filter { path -> !path.containsAny(setOf("x86_64-linux", "universal-darwin")) }
            .toMutableSet()
    }

    override fun isValidSdkHome(path: String): Boolean {
        // We expect the location of the executables, wherever that is.
        // This is different from a TexliveSdk installation, where we have the parent directory of the TeX Live installation and find everything there.
        return LatexSdkUtil.isPdflatexPresent(path)
    }

    override fun getInvalidHomeMessage(path: String) = "Could not find $path/pdflatex"

    override fun getVersionString(sdkHome: String): String {
        // Assume pdflatex --version contains output of the form
        // pdfTeX 3.14159265-2.6-1.40.21 (TeX Live 2020/mydistro)
        val output = LatexSdkUtil.parsePdflatexOutput(runCommand("$sdkHome/pdflatex", "--version") ?: "")
        return """TeX Live (\d\d\d\d).*""".toRegex().find(output)?.value ?: "Unknown version"
    }

    override fun getDefaultDocumentationUrl(sdk: Sdk): String {
        return "$texmfDistPath/doc"
    }

    override fun getExecutableName(executable: String, homePath: String): String {
        return "$homePath/$executable"
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.util.runCommand
import java.io.File

/**
 * Tectonic has its own source, which is in effect TeX Live, but since it is stored not in TeX Live format
 * but in a Tectonic-specific cache it warrants its own SDK.
 */
class TectonicSdk : LatexSdk("Tectonic SDK") {

    companion object {

        // Map readable file name (e.g. article.sty) to actual file path on disk
        var fileLocationCache: Map<String, String>? = null
    }

    /**
     * Get the location (full path) of a package, given its name.
     * Returns empty string if the location is unknown.
     */
    fun getPackageLocation(name: String, homePath: String?): String {
        if (homePath == null) return ""

        // Avoid many threads trying to fill the cache at the same time
        synchronized(this) {
            if (fileLocationCache == null) {
                fileLocationCache = File("$homePath/urls").listFiles()
                    // Get manifest names
                    ?.mapNotNull { it.readText().trim() }
                    // Get manifest contents
                    ?.flatMap { File("$homePath/manifests/$it.txt").readLines().map { line -> line.trim() } }
                    // Example line: article.sty 1741 9697d28bf5cc3d2f...
                    ?.map { it.split(" ").filter { word -> word.isNotBlank() } }
                    ?.filter { it.size >= 2 }
                    // Map human readable file name to file name on disk
                    ?.associate { Pair(it.first(), it.last()) }
                    // Tectonic stores files like this, currently
                    ?.mapValues { "$homePath/files/${it.value.take(2)}/${it.value.drop(2)}" } ?: return ""
            }
        }
        return fileLocationCache?.get(name) ?: ""
    }

    override fun getLatexDistributionType() = LatexDistributionType.TEXLIVE

    // We assume Tectonic is in PATH.
    override fun getExecutableName(executable: String, homePath: String) = executable

    override fun suggestHomePath(): String {
        // https://github.com/tectonic-typesetting/tectonic/issues/159
        val home = System.getProperty("user.home")
        return if (SystemInfo.isMac) "$home/Library/Caches/Tectonic"
        else if (SystemInfo.isWindows) "$home/TectonicProject/Tectonic" // Did not test if this works
        else "$home/.cache/Tectonic"
    }

    // The home path we are interested in, is actually the cache path, as it contains all the LaTeX files.
    override fun isValidSdkHome(path: String): Boolean {
        return File(path).run {
            // We are looking for a urls directory as bootstrap
            exists() && isDirectory && listFiles()?.map { it.name }?.contains("urls") == true
        }
    }

    override fun getInvalidHomeMessage(path: String) = "Please select the caches path for Tectonic"

    // Actually we should return the TeX Live version, but not sure how to find it
    override fun getVersionString(sdkHome: String): String? {
        return "tectonic -V".runCommand()?.replace("Tectonic", "", ignoreCase = true)?.trim()
    }

    // Actually the dtx files are not cached, just lots of other file types.
    // sty files do seem to be present, but at the moment we're not indexing those.
    override fun getDefaultSourcesPath(homePath: String): VirtualFile? {
        return LocalFileSystem.getInstance().findFileByPath("$homePath/files")
    }

    override fun getDefaultStyleFilesPath(homePath: String): VirtualFile? {
        return getDefaultSourcesPath(homePath)
    }
}

package nl.hannahsten.texifyidea.settings.sdk

import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.util.runCommand
import java.io.File
import java.util.*

/**
 * TeX Live, as installed in the recommended way by https://www.tug.org/texlive/quickinstall.html (Windows or Linux).
 */
open class TexliveSdk(name: String = "TeX Live SDK") : LatexSdk(name) {

    companion object {

        /**
         * Returns year of texlive installation, 0 if it is not texlive.
         * Assumes the pdflatex version output contains something like (TeX Live 2019).
         */
        val version: Int by lazy {
            if (!isAvailable) {
                0
            }
            else {
                val startIndex = LatexSdkUtil.pdflatexVersionText.indexOf("TeX Live")
                try {
                    LatexSdkUtil.pdflatexVersionText.substring(
                        startIndex + "TeX Live ".length,
                        startIndex + "TeX Live ".length + "2019".length
                    ).toInt()
                }
                catch (e: NumberFormatException) {
                    0
                }
            }
        }

        /**
         * Whether the user is using TeX Live or not.
         * This value is only computed once.
         */
        val isAvailable: Boolean by lazy {
            LatexSdkUtil.pdflatexVersionText.contains("TeX Live")
        }
    }

    override fun suggestHomePath(): String {
        // This method should work fast and allow running from the EDT thread.
        // It will be the starting point when someone opens the file explorer dialog to select an SDK of this type
        // Guess defaults without accessing file system
        val year = Calendar.getInstance().weekYear
        return if (SystemInfo.isMac) "/usr/local/texlive/$year" else "~/texlive/$year"
    }

    override fun suggestHomePaths(): MutableCollection<String> {
        // Note that suggested paths appear under "Detected SDK's" when adding an SDK
        val results = mutableSetOf(suggestHomePath())
        val paths = if (SystemInfo.isWindows) "where pdflatex".runCommand() else "which pdflatex".runCommand()
        if (!paths.isNullOrEmpty()) {
            for (path in paths.split("\\s+".toRegex())) {
                // Resolve symlinks
                val resolvedPath = runCommand("readlink", "-f", path) ?: path

                // We don't know for sure whether this path contains 'texlive':
                // e.g. C:\texnolive\2021\bin\pdflatex.exe can be perfectly valid
                if (resolvedPath.contains("miktex", ignoreCase = true)) {
                    continue
                }

                // Let's just assume that there is only one /bin/ in this path
                val index = resolvedPath.findLastAnyOf(setOf(File.separator + "bin" + File.separator))?.first ?: (resolvedPath.length - 1)
                if (index > 0) {
                    results.add(resolvedPath.substring(0, index))
                }
            }
        }

        return results
    }

    override fun isValidSdkHome(path: String): Boolean {
        // We expect the location of the LaTeX installation, for example ~/texlive/2020

        // If this is a valid LaTeX installation, pdflatex should be present in a subfolder in bin, e.g. $path/bin/x86_64-linux/pdflatex
        val parent = LatexSdkUtil.getPdflatexParentPath(path)
        return LatexSdkUtil.isPdflatexPresent(parent)
    }

    override fun getInvalidHomeMessage(path: String) = "Could not find $path/bin/*/pdflatex"

    override fun getLatexDistributionType() = LatexDistributionType.TEXLIVE

    override fun getVersionString(sdkHome: String): String {
        return "TeX Live " + sdkHome.split("/").lastOrNull { it.isNotBlank() }
    }

    override fun getDefaultDocumentationUrl(sdk: Sdk): String? {
        if (sdk.homePath == null) return null
        return sdk.homePath
    }

    override fun getDefaultSourcesPath(homePath: String): VirtualFile? {
        return LocalFileSystem.getInstance().findFileByPath("$homePath/texmf-dist/source/latex")
    }

    override fun getDefaultStyleFilesPath(homePath: String): VirtualFile? {
        return LocalFileSystem.getInstance().findFileByPath("$homePath/texmf-dist/tex/latex")
    }

    override fun getExecutableName(executable: String, homePath: String): String {
        // Get base path of LaTeX distribution
        val basePath = LatexSdkUtil.getPdflatexParentPath(homePath)
        return if (basePath != null) "$basePath/$executable" else executable
    }
}

package nl.hannahsten.texifyidea.structure.bibtex

import com.intellij.psi.PsiElement
import com.intellij.ui.breadcrumbs.BreadcrumbsProvider
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexId
import nl.hannahsten.texifyidea.psi.BibtexKey
import nl.hannahsten.texifyidea.psi.BibtexTag
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.substringEnd
import nl.hannahsten.texifyidea.util.tokenType
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class BibtexBreadcrumbsInfo : BreadcrumbsProvider {

    override fun getLanguages() = arrayOf(BibtexLanguage)

    override fun getElementInfo(element: PsiElement) = when (element) {
        is BibtexEntry -> {
            val token = element.tokenType()
            val identifier = when (token.lowercase(Locale.getDefault())) {
                "@preamble" -> ""
                "@string" -> element.firstChildOfType(BibtexKey::class)?.text
                else -> element.firstChildOfType(BibtexId::class)?.text?.substringEnd(1)
            } ?: ""

            if (identifier.isEmpty()) {
                token
            }
            else {
                "$token($identifier)"
            }
        }
        is BibtexTag -> {
            element.text
        }
        else -> ""
    } ?: ""

    override fun acceptElement(element: PsiElement) = when (element) {
        is BibtexEntry -> true
        is BibtexTag -> true
        else -> false
    }
}

package nl.hannahsten.texifyidea.structure.bibtex

import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.structure.latex.LatexFilePresentation

/**
 * @author Hannah Schellekens
 */
open class BibtexFilePresentation(val file: PsiFile) : LatexFilePresentation(file) {

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_BIB
}

package nl.hannahsten.texifyidea.structure.bibtex

import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.util.treeView.smartTree.SortableTreeElement
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import com.intellij.navigation.NavigationItem
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.BibtexFile
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class BibtexStructureViewElement(val element: PsiElement) : StructureViewTreeElement, SortableTreeElement {

    override fun getValue() = element

    override fun navigate(requestFocus: Boolean) {
        if (element is NavigationItem) {
            element.navigate(requestFocus)
        }
    }

    override fun canNavigate() = element is NavigationItem && element.canNavigate()

    override fun canNavigateToSource() = element is NavigationItem && element.canNavigateToSource()

    override fun getAlphaSortKey() = when (element) {
        is PsiFile -> element.name.lowercase(Locale.getDefault())
        else -> element.text.lowercase(Locale.getDefault())
    }

    override fun getPresentation(): ItemPresentation {
        if (element is BibtexFile) {
            return BibtexFilePresentation(element)
        }

        throw AssertionError("Should not happen: element !is BibtexFile.")
    }

    override fun getChildren(): Array<TreeElement> {
        if (element !is BibtexFile) {
            return emptyArray()
        }

        return arrayOf(BibtexStructureViewEntriesElement(element))
    }
}

package nl.hannahsten.texifyidea.structure.bibtex

import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.util.treeView.smartTree.SortableTreeElement
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import com.intellij.psi.PsiFile
import com.intellij.util.PlatformIcons
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.parser.childrenOfType

/**
 * Contains all the entries. Class exist to prevent auto-collapsing.
 *
 * @author Hannah Schellekens
 */
open class BibtexStructureViewEntriesElement(val file: PsiFile) : StructureViewTreeElement, SortableTreeElement {

    private val entriesPresentation: ItemPresentation = object : ItemPresentation {

        override fun getLocationString() = file.childrenOfType(BibtexEntry::class).size.toString()

        override fun getPresentableText() = "entries"

        override fun getIcon(b: Boolean) = PlatformIcons.ANNOTATION_TYPE_ICON
    }

    override fun getValue() = file

    override fun navigate(requestFocus: Boolean) = file.navigate(requestFocus)

    override fun canNavigate() = file.canNavigate()

    override fun canNavigateToSource() = file.canNavigateToSource()

    override fun getAlphaSortKey() = "entries"

    override fun getPresentation() = entriesPresentation

    override fun getChildren(): Array<TreeElement> {
        val entries = file.childrenOfType(BibtexEntry::class)
        return entries
            .map { BibtexStructureViewEntryElement(it) }
            .toTypedArray()
    }
}

package nl.hannahsten.texifyidea.structure.bibtex

import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.util.treeView.smartTree.SortableTreeElement
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.NavigationItem
import com.intellij.util.PlatformIcons
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.parser.getIdentifier
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class BibtexStructureViewEntryElement(val entry: BibtexEntry) : StructureViewTreeElement, SortableTreeElement {

    private val entryPresentation: PresentationData = object : PresentationData() {

        override fun getLocationString() = when (entry.tokenName().lowercase(Locale.getDefault())) {
            "string" -> entry.tags().first().content?.text.orEmpty()
            "preamble" -> ""
            else -> entry.tokenName()
        }

        override fun getPresentableText() = when (entry.tokenName().lowercase(Locale.getDefault())) {
            "preamble" -> "preamble"
            "string" -> entry.tags().firstOrNull()?.keyName()
            else -> entry.getIdentifier()
        } ?: ""

        override fun getIcon(b: Boolean) = when (entry.tokenName().lowercase(Locale.getDefault())) {
            "string" -> TexifyIcons.STRING
            "preamble" -> PlatformIcons.PROPERTY_ICON
            else -> PlatformIcons.ANNOTATION_TYPE_ICON
        }
    }

    override fun getValue() = entry

    override fun navigate(requestFocus: Boolean) {
        if (entry is NavigationItem) {
            entry.navigate(requestFocus)
        }
    }

    override fun canNavigate() = entry is NavigationItem && entry.canNavigate()

    override fun canNavigateToSource() = entry is NavigationItem && entry.canNavigateToSource()

    override fun getAlphaSortKey() = when (entry.tokenName()) {
        "preamble" -> "a"
        "string" -> "b"
        else -> "c"
    } + entry.getIdentifier()

    override fun getPresentation() = entryPresentation

    override fun getChildren(): Array<TreeElement> {
        if (entry.isString() || entry.isPreamble()) {
            return emptyArray()
        }

        return entry.tags().map { BibtexStructureViewTagElement(it) }.toTypedArray()
    }
}

package nl.hannahsten.texifyidea.structure.bibtex

import com.intellij.ide.structureView.StructureViewModel
import com.intellij.ide.structureView.TreeBasedStructureViewBuilder
import com.intellij.lang.PsiStructureViewFactory
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.psi.StructurePsiChangeListener

/**
 * @author Hannah Schellekens
 */
open class BibtexStructureViewFactory : PsiStructureViewFactory {

    override fun getStructureViewBuilder(file: PsiFile) = object : TreeBasedStructureViewBuilder() {

        override fun createStructureViewModel(editor: Editor?): StructureViewModel {
            val project = editor?.project ?: return BibtexStructureViewModel(file, editor)
            val manager = PsiManager.getInstance(project)
            manager.addPsiTreeChangeListener(StructurePsiChangeListener(project)) { }

            return BibtexStructureViewModel(file, editor)
        }
    }
}

package nl.hannahsten.texifyidea.structure.bibtex

import com.intellij.ide.structureView.StructureViewModel.ElementInfoProvider
import com.intellij.ide.structureView.StructureViewModelBase
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.util.treeView.smartTree.Sorter
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.structure.filter.EntryFilter
import nl.hannahsten.texifyidea.structure.filter.PreambleFilter
import nl.hannahsten.texifyidea.structure.filter.StringFilter

/**
 * @author Hannah Schellekens
 */
open class BibtexStructureViewModel(
    val file: PsiFile,
    theEditor: Editor?
) : StructureViewModelBase(file, theEditor, BibtexStructureViewElement(file)), ElementInfoProvider {

    companion object {

        val sorterArray = arrayOf(Sorter.ALPHA_SORTER)
        val filterArray = arrayOf(EntryFilter, StringFilter, PreambleFilter)
    }

    override fun getSorters() = sorterArray

    override fun getFilters() = filterArray

    override fun isAlwaysShowsPlus(element: StructureViewTreeElement?) = false

    override fun isAlwaysLeaf(element: StructureViewTreeElement?) = false
}

package nl.hannahsten.texifyidea.structure.bibtex

import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.util.treeView.smartTree.SortableTreeElement
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.NavigationItem
import com.intellij.util.PlatformIcons
import nl.hannahsten.texifyidea.psi.BibtexTag
import nl.hannahsten.texifyidea.util.evaluate
import nl.hannahsten.texifyidea.util.keyName

/**
 * @author Hannah Schellekens
 */
open class BibtexStructureViewTagElement(val tag: BibtexTag) : StructureViewTreeElement, SortableTreeElement {

    private val tagPresentation: PresentationData = object : PresentationData() {

        override fun getLocationString() = tag.content?.evaluate().orEmpty()

        override fun getPresentableText() = tag.keyName()

        override fun getIcon(b: Boolean) = PlatformIcons.PROTECTED_ICON
    }

    override fun getValue() = tag

    override fun navigate(requestFocus: Boolean) {
        if (tag is NavigationItem) {
            tag.navigate(requestFocus)
        }
    }

    override fun canNavigate() = tag is NavigationItem && tag.canNavigate()

    override fun canNavigateToSource() = tag is NavigationItem && tag.canNavigateToSource()

    override fun getAlphaSortKey() = tag.keyName() ?: ""

    override fun getPresentation() = tagPresentation

    override fun getChildren(): Array<TreeElement> = emptyArray()
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.ActionPresentation
import com.intellij.ide.util.treeView.smartTree.Filter
import com.intellij.ide.util.treeView.smartTree.TreeElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.structure.latex.LatexStructureViewCommandElement

/**
 * @author Hannah Schellekens
 */
class BibitemFilter : Filter {

    override fun isVisible(treeElement: TreeElement): Boolean {
        return if (treeElement !is LatexStructureViewCommandElement) {
            true
        }
        else treeElement.commandName != "\\bibitem"
    }

    override fun isReverted() = true

    override fun getPresentation() = BibitemFilterPresentation

    override fun getName() = "latex.texify.filter.bibitem"

    /**
     * @author Hannah Schellekens
     */
    object BibitemFilterPresentation : ActionPresentation {

        override fun getText() = "Show Bibliography Items"

        override fun getDescription() = "Show bibliography items"

        override fun getIcon() = TexifyIcons.DOT_BIB
    }
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.ActionPresentation
import com.intellij.ide.util.treeView.smartTree.Filter
import com.intellij.ide.util.treeView.smartTree.TreeElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.structure.latex.LatexOtherCommandPresentation
import nl.hannahsten.texifyidea.structure.latex.LatexStructureViewCommandElement
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * @author Hannah Schellekens
 */
class CommandDefinitionFilter : Filter {

    override fun isVisible(treeElement: TreeElement): Boolean {
        return if (treeElement !is LatexStructureViewCommandElement) {
            true
        }
        else !(
            treeElement.commandName == "\\newcommand" ||
                treeElement.commandName in CommandMagic.mathCommandDefinitions ||
                treeElement.presentation is LatexOtherCommandPresentation
            )
    }

    override fun isReverted() = true

    override fun getPresentation() = LatexNewCommandFilterPresentation

    override fun getName() = "latex.texify.filter.newcommand"

    /**
     * @author Hannah Schellekens
     */
    object LatexNewCommandFilterPresentation : ActionPresentation {

        override fun getText() = "Show Command Definitions"

        override fun getDescription() = "Show command definitions"

        override fun getIcon() = TexifyIcons.DOT_COMMAND
    }
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import javax.swing.Icon

/**
 * @author Hannah Schellekens
 */
enum class DummyTreeElement : TreeElement {

    INSTANCE;

    override fun getPresentation() = DummyPresentation

    override fun getChildren(): Array<out TreeElement> = TreeElement.EMPTY_ARRAY

    /**
     * @author Hannah Schellekens
     */
    object DummyPresentation : ItemPresentation {

        override fun getPresentableText(): String? = null

        override fun getLocationString(): String? = null

        override fun getIcon(b: Boolean): Icon? = null
    }
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.ActionPresentation
import com.intellij.ide.util.treeView.smartTree.Filter
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.util.PlatformIcons
import nl.hannahsten.texifyidea.structure.bibtex.BibtexStructureViewEntryElement
import nl.hannahsten.texifyidea.util.tokenName
import java.util.*

/**
 * @author Hannah Schellekens
 */
object EntryFilter : Filter {

    override fun isVisible(treeElement: TreeElement?): Boolean {
        if (treeElement !is BibtexStructureViewEntryElement) {
            return true
        }

        val name = treeElement.entry.tokenName().lowercase(Locale.getDefault())
        return name == "string" || name == "preamble"
    }

    override fun isReverted() = true

    override fun getName() = "bibtex.texify.filter.entry"

    override fun getPresentation() = object : ActionPresentation {

        override fun getText() = "Show Entries"

        override fun getDescription() = "Show entries"

        override fun getIcon() = PlatformIcons.ANNOTATION_TYPE_ICON
    }
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.ActionPresentation
import com.intellij.ide.util.treeView.smartTree.Filter
import com.intellij.ide.util.treeView.smartTree.TreeElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.structure.latex.LatexStructureViewCommandElement
import nl.hannahsten.texifyidea.util.getIncludeCommands

/**
 * @author Hannah Schellekens
 */
class IncludesFilter : Filter {

    override fun isVisible(treeElement: TreeElement): Boolean {
        return if (treeElement !is LatexStructureViewCommandElement) {
            true
        }
        else treeElement.commandName !in getIncludeCommands()
    }

    override fun isReverted() = true

    override fun getPresentation() = LatexIncludesFilterPresentation

    override fun getName() = "latex.texify.filter.includes"

    /**
     * @author Hannah Schellekens
     */
    object LatexIncludesFilterPresentation : ActionPresentation {

        override fun getText() = "Show Includes"

        override fun getDescription() = "Show includes"

        override fun getIcon() = TexifyIcons.DOT_INCLUDE
    }
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.ActionPresentation
import com.intellij.ide.util.treeView.smartTree.Filter
import com.intellij.ide.util.treeView.smartTree.TreeElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.structure.latex.LatexStructureViewCommandElement
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands
import javax.swing.Icon

/**
 * @author Hannah Schellekens
 */
class LabelFilter : Filter {

    override fun isVisible(treeElement: TreeElement): Boolean {
        if (treeElement !is LatexStructureViewCommandElement) {
            return true
        }
        return !getLabelDefinitionCommands().contains(treeElement.commandName)
    }

    override fun isReverted(): Boolean = true

    override fun getPresentation(): ActionPresentation = LatexLabelFilterPresentation.INSTANCE

    override fun getName(): String = "latex.texify.filter.label"

    /**
     * @author Hannah Schellekens
     */
    private class LatexLabelFilterPresentation : ActionPresentation {

        override fun getText(): String = "Show Labels"

        override fun getDescription(): String = "Show labels"

        override fun getIcon(): Icon = TexifyIcons.DOT_LABEL

        companion object {

            val INSTANCE = LatexLabelFilterPresentation()
        }
    }
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.ActionPresentation
import com.intellij.ide.util.treeView.smartTree.Filter
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.util.PlatformIcons
import nl.hannahsten.texifyidea.structure.bibtex.BibtexStructureViewEntryElement
import nl.hannahsten.texifyidea.util.tokenName
import java.util.*

/**
 * @author Hannah Schellekens
 */
object PreambleFilter : Filter {

    override fun isVisible(treeElement: TreeElement?): Boolean {
        if (treeElement !is BibtexStructureViewEntryElement) {
            return true
        }

        return treeElement.entry.tokenName().lowercase(Locale.getDefault()) != "preamble"
    }

    override fun isReverted() = true

    override fun getName() = "bibtex.texify.filter.preamble"

    override fun getPresentation() = object : ActionPresentation {

        @Suppress("DialogTitleCapitalization")
        override fun getText() = "Show @preamble"

        override fun getDescription() = "Show @preamble"

        override fun getIcon() = PlatformIcons.PROPERTY_ICON
    }
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.ActionPresentation
import com.intellij.ide.util.treeView.smartTree.Filter
import com.intellij.ide.util.treeView.smartTree.TreeElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.structure.latex.LatexStructureViewCommandElement
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * @author Hannah Schellekens
 */
class SectionFilter : Filter {

    override fun isVisible(treeElement: TreeElement): Boolean {
        return if (treeElement !is LatexStructureViewCommandElement) {
            true
        }
        else !CommandMagic.sectionMarkers.contains(treeElement.commandName)
    }

    override fun isReverted() = true

    override fun getPresentation() = LatexSectionFilterPresentation

    override fun getName() = "latex.texify.filter.section"

    /**
     * @author Hannah Schellekens
     */
    object LatexSectionFilterPresentation : ActionPresentation {

        override fun getText() = "Show Sectioning"

        override fun getDescription() = "Show sectioning"

        override fun getIcon() = TexifyIcons.DOT_SECTION
    }
}

package nl.hannahsten.texifyidea.structure.filter

import com.intellij.ide.util.treeView.smartTree.ActionPresentation
import com.intellij.ide.util.treeView.smartTree.Filter
import com.intellij.ide.util.treeView.smartTree.TreeElement
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.structure.bibtex.BibtexStructureViewEntryElement
import nl.hannahsten.texifyidea.util.tokenName
import java.util.*

/**
 * @author Hannah Schellekens
 */
object StringFilter : Filter {

    override fun isVisible(treeElement: TreeElement?): Boolean {
        if (treeElement !is BibtexStructureViewEntryElement) {
            return true
        }

        return treeElement.entry.tokenName().lowercase(Locale.getDefault()) != "string"
    }

    override fun isReverted() = true

    override fun getName() = "bibtex.texify.filter.string"

    override fun getPresentation() = object : ActionPresentation {

        @Suppress("DialogTitleCapitalization")
        override fun getText() = "Show @string"

        override fun getDescription() = "Show @string"

        override fun getIcon() = TexifyIcons.STRING
    }
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.fileEditor.FileDocumentManager
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * @author Hannah Schellekens
 */
class BibitemPresentation(labelCommand: LatexCommands) : ItemPresentation {

    private val bibitemName: String
    private val locationString: String

    init {
        if (labelCommand.commandToken.text != "\\bibitem") {
            throw IllegalArgumentException("command is no \\bibitem-command")
        }

        // Get label name.
        this.bibitemName = labelCommand.getRequiredParameters().firstOrNull() ?: ""

        // Location string.
        val manager = FileDocumentManager.getInstance()
        val document = manager.getDocument(labelCommand.containingFile.virtualFile)
        val line = document!!.getLineNumber(labelCommand.textOffset) + 1
        this.locationString = labelCommand.containingFile.name + ":" + line
    }

    override fun getPresentableText() = bibitemName

    override fun getLocationString() = locationString

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_BIB
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.psi.PsiElement
import com.intellij.ui.breadcrumbs.BreadcrumbsProvider
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.parser.name

/**
 * @author Hannah Schellekens
 */
open class LatexBreadcrumbsInfo : BreadcrumbsProvider {

    override fun getLanguages() = arrayOf(LatexLanguage)

    override fun getElementInfo(element: PsiElement) = when (element) {
        is LatexEnvironment -> element.name()?.text
        is LatexCommands -> element.commandToken.text
        else -> ""
    } ?: ""

    override fun acceptElement(element: PsiElement) = when (element) {
        is LatexEnvironment -> true
        is LatexCommands -> true
        else -> false
    }
}

package nl.hannahsten.texifyidea.structure.latex

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation
import nl.hannahsten.texifyidea.util.magic.cmd

/**
 * @author Hannah Schellekens
 */
class LatexChapterPresentation(chapterCommand: LatexCommands) : EditableHintPresentation {

    private val chapterName: String
    private var hint = ""

    init {
        if (chapterCommand.name != LatexGenericRegularCommand.CHAPTER.cmd) {
            throw IllegalArgumentException("command is no \\chapter-command")
        }

        this.chapterName = chapterCommand.getRequiredParameters().getOrElse(0) { "No chapter name" }
    }

    override fun getPresentableText() = chapterName

    override fun getLocationString() = hint

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_CHAPTER

    override fun setHint(hint: String) {
        this.hint = hint
    }
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.navigation.ItemPresentation
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.util.magic.PatternMagic

/**
 * @author Hannah Schellekens
 */
open class LatexFilePresentation(private val file: PsiFile) : ItemPresentation {

    private val presentableText = PatternMagic.fileExtension.matcher(file.name).replaceAll("")

    override fun getPresentableText() = presentableText!!

    override fun getLocationString() = file.virtualFile.path

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_LATEX
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.navigation.ItemPresentation
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.getIncludeCommands
import nl.hannahsten.texifyidea.util.parser.getIncludedFiles

/**
 * @author Hannah Schellekens
 */
class LatexIncludePresentation(labelCommand: LatexCommands) : ItemPresentation {

    private val fileName: String

    init {
        if (labelCommand.name !in getIncludeCommands()) {
            throw IllegalArgumentException("Command $labelCommand is no include command")
        }

        this.fileName = labelCommand.getIncludedFiles(true).joinToString { it.name }
    }

    override fun getPresentableText() = fileName

    override fun getLocationString() = ""

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_INCLUDE
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.fileEditor.FileDocumentManager
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * @author Hannah Schellekens
 */
class LatexLabelPresentation(labelCommand: LatexCommands) : ItemPresentation {

    private val locationString: String
    private val presentableText: String

    init {
        val labelingCommands = getLabelDefinitionCommands()
        if (!labelingCommands.contains(labelCommand.commandToken.text)) {
            val token = labelCommand.commandToken.text
            throw IllegalArgumentException("command '$token' is no \\label-command")
        }

        val position =
            CommandManager.labelAliasesInfo.getOrDefault(labelCommand.commandToken.text, null)?.positions?.firstOrNull()
                ?: 0
        presentableText = labelCommand.requiredParameter(position) ?: "no label found"

        // Location string.
        val manager = FileDocumentManager.getInstance()
        val document = manager.getDocument(labelCommand.containingFile.virtualFile)
        val line = document!!.getLineNumber(labelCommand.textOffset) + 1
        this.locationString = labelCommand.containingFile.name + ":" + line
    }

    override fun getPresentableText() = presentableText

    override fun getLocationString() = locationString

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_LABEL
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.navigation.ItemPresentation
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.lang.commands.LatexNewDefinitionCommand
import nl.hannahsten.texifyidea.lang.commands.LatexXparseCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.toStringMap
import nl.hannahsten.texifyidea.util.parser.nextCommand

/**
 * @author Hannah Schellekens
 */
class LatexNewCommandPresentation(newCommand: LatexCommands) : ItemPresentation {

    private val newCommandName: String
    private val locationString: String

    init {
        // Fetch parameter amount.
        val optional = newCommand.getOptionalParameterMap().toStringMap().keys.toList()
        var params = -1
        if (optional.isNotEmpty()) {
            try {
                params = Integer.parseInt(optional[0])
            }
            catch (ignored: NumberFormatException) {
            }
        }
        val suffix = if (params != -1) "{x$params}" else ""

        // Get command name.
        val required = newCommand.getRequiredParameters()
        val command = if (required.isNotEmpty()) {
            required.first()
        }
        else {
            // If there are no required parameters, the user may have left out the braces around the first one, so we get it manually
            newCommand.nextCommand()?.commandToken?.text
        }

        this.newCommandName = command ?: ("" + suffix)

        // Get the definition to show in place of the location string.
        locationString = when {
            newCommand.commandToken.text == "\\" + LatexNewDefinitionCommand.NEWCOMMAND.command && required.size >= 2 -> required[1]
            newCommand.commandToken.text == "\\" + LatexXparseCommand.NEWDOCUMENTCOMMAND.command && required.size >= 3 -> required[2]
            else -> ""
        }
    }

    override fun getPresentableText() = newCommandName

    override fun getLocationString() = locationString

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_COMMAND
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.navigation.ItemPresentation
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.nextCommand
import javax.swing.Icon

/**
 * @author Hannah Schellekens
 */
class LatexOtherCommandPresentation(command: LatexCommands, private val icon: Icon) : ItemPresentation {

    private val commandName = command.name
    private var locationString: String

    init {
        val firstNext = command.nextCommand()
        if (firstNext != null) {
            val lookup = firstNext.commandToken.text
            this.locationString = lookup ?: ""
        }

        locationString = ""
    }

    override fun getPresentableText() = commandName

    override fun getLocationString() = locationString

    override fun getIcon(b: Boolean) = icon
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.navigation.ItemPresentation
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands

class LatexPairedDelimiterPresentation(newCommand: LatexCommands) : ItemPresentation {

    private val newCommandName: String
    private val locationString: String

    init {
        // Get command name.
        val required = newCommand.getRequiredParameters()
        newCommandName = if (required.isNotEmpty()) {
            required.first()
        }
        else ""

        locationString = if (required.size >= 3) {
            when (newCommand.name) {
                "\\DeclarePairedDelimiterXPP" -> (1..4).joinToString(" ") { required[it] }
                else -> "${required[1]} ${required[2]}"
            }
        }
        else ""
    }

    override fun getPresentableText() = newCommandName

    override fun getLocationString() = locationString

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_COMMAND
}

package nl.hannahsten.texifyidea.structure.latex

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation

/**
 * @author Hannah Schellekens
 */
class LatexParagraphPresentation(paragraphCommand: LatexCommands) : EditableHintPresentation {

    private val paragraphName: String
    private var hint = ""

    init {
        if (paragraphCommand.commandToken.text != "\\paragraph") {
            throw IllegalArgumentException("command is no \\paragraph-command")
        }

        if (paragraphCommand.getRequiredParameters().isEmpty()) {
            this.paragraphName = ""
        }
        else {
            this.paragraphName = paragraphCommand.getRequiredParameters()[0]
        }
    }

    override fun getPresentableText() = paragraphName

    override fun getLocationString() = hint

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_PARAGRAPH

    override fun setHint(hint: String) {
        this.hint = hint
    }
}

package nl.hannahsten.texifyidea.structure.latex

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation

/**
 * @author Hannah Schellekens
 */
class LatexPartPresentation(partCommand: LatexCommands) : EditableHintPresentation {

    private val partName: String
    private var hint = ""

    init {
        if (partCommand.commandToken.text != "\\part") {
            throw IllegalArgumentException("command is no \\part-command")
        }

        this.partName = partCommand.getRequiredParameters()[0]
    }

    override fun getPresentableText() = partName

    override fun getLocationString() = hint

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_PART

    override fun setHint(hint: String) {
        this.hint = hint
    }
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.navigation.ItemPresentation
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexMathtoolsRegularCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexNewDefinitionCommand.NEWCOMMAND
import nl.hannahsten.texifyidea.lang.commands.LatexXparseCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.getIncludeCommands
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands
import nl.hannahsten.texifyidea.util.magic.cmd

/**
 * @author Hannah Schellekens
 */
object LatexPresentationFactory {

    @JvmStatic
    fun getPresentation(commands: LatexCommands): ItemPresentation {
        val labelingCommands = commands.project.getLabelDefinitionCommands()
        if (labelingCommands.contains(commands.name)) {
            return LatexLabelPresentation(commands)
        }
        return when (commands.name) {
            PART.cmd -> LatexPartPresentation(commands)
            CHAPTER.cmd -> LatexChapterPresentation(commands)
            SECTION.cmd -> LatexSectionPresentation(commands)
            SUBSECTION.cmd -> LatexSubSectionPresentation(commands)
            SUBSUBSECTION.cmd -> LatexSubSubSectionPresentation(commands)
            PARAGRAPH.cmd -> LatexParagraphPresentation(commands)
            SUBPARAGRAPH.cmd -> LatexSubParagraphPresentation(commands)
            NEWCOMMAND.cmd, DECLARE_MATH_OPERATOR.cmd, LatexXparseCommand.NEWDOCUMENTCOMMAND.cmd -> LatexNewCommandPresentation(commands)
            DECLARE_PAIRED_DELIMITER.cmd, DECLARE_PAIRED_DELIMITER_X.cmd, DECLARE_PAIRED_DELIMITER_XPP.cmd -> LatexPairedDelimiterPresentation(
                commands
            )
            LABEL.cmd -> LatexLabelPresentation(commands)
            BIBITEM.cmd -> BibitemPresentation(commands)
            in getIncludeCommands() -> LatexIncludePresentation(commands)
            else -> LatexOtherCommandPresentation(commands, TexifyIcons.DOT_COMMAND)
        }
    }
}

package nl.hannahsten.texifyidea.structure.latex

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation

/**
 * @author Hannah Schellekens
 */
class LatexSectionPresentation(sectionCommand: LatexCommands) : EditableHintPresentation {

    private val sectionName: String
    private var hint = ""

    init {
        if (sectionCommand.commandToken.text != "\\section") {
            throw IllegalArgumentException("command is no \\section-command")
        }

        this.sectionName = sectionCommand.getRequiredParameters()[0]
    }

    override fun getPresentableText() = sectionName

    override fun getLocationString() = hint

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_SECTION

    override fun setHint(hint: String) {
        this.hint = hint
    }
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.util.treeView.smartTree.SortableTreeElement
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.NavigationItem
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation
import nl.hannahsten.texifyidea.util.parser.nextCommand
import java.util.*

/**
 * @author Hannah Schellekens
 */
class LatexStructureViewCommandElement private constructor(private val element: LatexCommands) : StructureViewTreeElement, SortableTreeElement {

    companion object {

        @JvmStatic
        fun newCommand(commands: LatexCommands): LatexStructureViewCommandElement? {
            if ("\\let" == commands.commandToken.text || "\\def" == commands.commandToken.text) {
                val sibling = commands.nextCommand() ?: return null

                return LatexStructureViewCommandElement(sibling)
            }

            return LatexStructureViewCommandElement(commands)
        }
    }

    private val sectionChildren = ArrayList<TreeElement>()
    private val presentation = LatexPresentationFactory.getPresentation(element)

    val commandName: String
        get() = element.commandToken.text

    fun addChild(child: TreeElement) {
        sectionChildren.add(child)
    }

    fun setHint(hint: String) {
        (presentation as? EditableHintPresentation)?.setHint(hint)
    }

    override fun getValue() = element

    override fun getAlphaSortKey(): String {
        val text = presentation.presentableText
        return text ?: ""
    }

    override fun getPresentation() = presentation

    override fun getChildren() = sectionChildren.toTypedArray()

    override fun navigate(requestFocus: Boolean) {
        if (element is NavigationItem) {
            (element as NavigationItem).navigate(requestFocus)
        }
    }

    override fun canNavigate(): Boolean {
        return element is NavigationItem && (element as NavigationItem).canNavigate()
    }

    override fun canNavigateToSource(): Boolean {
        return element is NavigationItem && (element as NavigationItem).canNavigateToSource()
    }
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.util.treeView.smartTree.SortableTreeElement
import com.intellij.ide.util.treeView.smartTree.TreeElement
import com.intellij.navigation.ItemPresentation
import com.intellij.navigation.NavigationItem
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import nl.hannahsten.texifyidea.file.*
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.structure.bibtex.BibtexStructureViewElement
import nl.hannahsten.texifyidea.structure.latex.SectionNumbering.DocumentClass
import nl.hannahsten.texifyidea.util.parser.allCommands
import nl.hannahsten.texifyidea.util.getIncludeCommands
import nl.hannahsten.texifyidea.util.parser.getIncludedFiles
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import java.util.*
import kotlin.collections.ArrayList

/**
 * @author Hannah Schellekens
 */
class LatexStructureViewElement(private val element: PsiElement) : StructureViewTreeElement, SortableTreeElement {

    override fun getValue() = element

    override fun canNavigate() = element is NavigationItem && (element as NavigationItem).canNavigate()

    override fun canNavigateToSource() = element is NavigationItem && (element as NavigationItem).canNavigateToSource()

    override fun navigate(requestFocus: Boolean) {
        if (element is NavigationItem) {
            (element as NavigationItem).navigate(requestFocus)
        }
    }

    override fun getAlphaSortKey(): String {
        return (element as? LatexCommands)?.commandToken?.text?.lowercase(Locale.getDefault())
            ?: if (element is PsiNameIdentifierOwner) {
                element.name!!.lowercase(Locale.getDefault())
            }
            else {
                element.text.lowercase(Locale.getDefault())
            }
    }

    override fun getPresentation(): ItemPresentation {
        if (element is LatexCommands) {
            return LatexPresentationFactory.getPresentation(element)
        }
        else if (element is PsiFile) {
            return LatexFilePresentation(element)
        }

        throw AssertionError("Should not happen! Element !is LatexCommands or PsiFile.")
    }

    override fun getChildren(): Array<TreeElement> {
        if (element !is LatexFile && element !is StyleFile && element !is ClassFile) {
            return emptyArray()
        }

        // Get document class.
        val scope = GlobalSearchScope.fileScope(element as PsiFile)
        val docClass = LatexCommandsIndex.getItems(element.getProject(), scope).asSequence()
            .filter { cmd -> cmd.commandToken.text == "\\documentclass" && cmd.getRequiredParameters().isNotEmpty() }
            .map { cmd -> cmd.getRequiredParameters()[0] }
            .firstOrNull() ?: "article"

        // Fetch all commands in the active file.
        val numbering = SectionNumbering(DocumentClass.getClassByName(docClass))
        val commands = element.allCommands()
        val treeElements = ArrayList<TreeElement>()

        // Add includes.
        addIncludes(treeElements, commands)

        // Add sectioning.
        val sections = ArrayDeque<LatexStructureViewCommandElement>()
        for (currentCmd in commands) {
            val token = currentCmd.commandToken.text

            // Update counter.
            if (token == "\\addtocounter" || token == "\\setcounter") {
                updateNumbering(currentCmd, numbering)
                continue
            }

            // Only consider section markers.
            if (!CommandMagic.sectionMarkers.contains(token)) {
                continue
            }

            if (currentCmd.getRequiredParameters().isEmpty()) {
                continue
            }

            val child = LatexStructureViewCommandElement.newCommand(currentCmd) ?: continue

            // First section.
            if (sections.isEmpty()) {
                sections.addFirst(child)
                treeElements.add(child)
                setLevelHint(child, numbering)
                continue
            }

            val currentIndex = order(current(sections) ?: continue)
            val nextIndex = order(currentCmd)

            when {
                currentIndex == nextIndex -> registerSameLevel(sections, child, currentCmd, treeElements, numbering)
                nextIndex > currentIndex -> registerDeeper(sections, child, numbering)
                else -> registerHigher(sections, child, currentCmd, treeElements, numbering)
            }
        }

        // Add command definitions.
        CommandMagic.commandDefinitionsAndRedefinitions.forEach {
            addFromCommand(treeElements, commands, it)
        }

        // Add label definitions.
        addFromLabelingCommands(treeElements, commands)

        // Add bibitem definitions.
        addFromCommand(treeElements, commands, "\\bibitem")

        return treeElements.toTypedArray()
    }

    private fun addIncludes(treeElements: MutableList<TreeElement>, commands: List<LatexCommands>) {
        for (command in commands) {
            if (command.name !in getIncludeCommands()) {
                continue
            }

            val elt = LatexStructureViewCommandElement.newCommand(command) ?: continue
            for (psiFile in command.getIncludedFiles(includeInstalledPackages = TexifySettings.getInstance().showPackagesInStructureView)) {
                if (BibtexFileType == psiFile.fileType) {
                    elt.addChild(BibtexStructureViewElement(psiFile))
                }
                else if (LatexFileType == psiFile.fileType || StyleFileType == psiFile.fileType) {
                    elt.addChild(LatexStructureViewElement(psiFile))
                }
            }
            treeElements.add(elt)
        }
    }

    private fun addFromCommand(
        treeElements: MutableList<TreeElement>, commands: List<LatexCommands>,
        commandName: String
    ) {
        for (cmd in commands) {
            if (cmd.commandToken.text != commandName) continue
            val element = LatexStructureViewCommandElement.newCommand(cmd) ?: continue
            treeElements.add(element)
        }
    }

    private fun addFromLabelingCommands(treeElements: MutableList<TreeElement>, commands: List<LatexCommands>) {
        val labelingCommands = getLabelDefinitionCommands()
        commands.filter { labelingCommands.contains(it.commandToken.text) }
            .mapNotNull { LatexStructureViewCommandElement.newCommand(it) }
            .forEach {
                treeElements.add(it)
            }
    }

    private fun registerHigher(
        sections: Deque<LatexStructureViewCommandElement>,
        child: LatexStructureViewCommandElement,
        currentCmd: LatexCommands,
        treeElements: MutableList<TreeElement>,
        numbering: SectionNumbering
    ) {
        val indexInsert = order(currentCmd)
        while (!sections.isEmpty()) {
            pop(sections)
            val index = current(sections)?.let { order(it) }

            if (index != null && indexInsert > index) {
                registerDeeper(sections, child, numbering)
                break
            }
            // Avoid that an element is not added at all by adding it one level up anyway.
            // If index is null, that means that the tree currently only has elements with a higher order.
            else {
                registerSameLevel(sections, child, currentCmd, treeElements, numbering)
                break
            }
        }
    }

    private fun registerDeeper(
        sections: Deque<LatexStructureViewCommandElement>,
        child: LatexStructureViewCommandElement,
        numbering: SectionNumbering
    ) {
        current(sections)?.addChild(child) ?: return
        queue(child, sections)

        setLevelHint(child, numbering)
    }

    private fun registerSameLevel(
        sections: Deque<LatexStructureViewCommandElement>,
        child: LatexStructureViewCommandElement,
        currentCmd: LatexCommands,
        treeElements: MutableList<TreeElement>,
        numbering: SectionNumbering
    ) {
        sections.pollFirst()
        val parent = sections.peekFirst()
        parent?.addChild(child)
        sections.addFirst(child)

        setLevelHint(child, numbering)

        if (currentCmd.commandToken.text == highestLevel(sections)) {
            treeElements.add(child)
        }
    }

    private fun setLevelHint(child: LatexStructureViewCommandElement, numbering: SectionNumbering) {
        if (hasStar(child.value)) {
            return
        }

        val level = order(child)
        numbering.increase(level)
        child.setHint(numbering.getTitle(level))
    }

    private fun updateNumbering(cmd: LatexCommands, numbering: SectionNumbering) {
        val token = cmd.commandToken.text
        val required = cmd.getRequiredParameters()
        if (required.size < 2) {
            return
        }

        // Get the level to modify.
        val name = required[0]
        val level = order("\\" + name)
        if (level == -1) {
            return
        }

        // Get the amount to modify with.
        val amount = required[1].toIntOrNull() ?: return

        if (token == "\\setcounter") {
            numbering.setCounter(level, amount)
        }
        else {
            numbering.addCounter(level, amount)
        }
    }

    private fun hasStar(commands: LatexCommands): Boolean {
        val leafs = PsiTreeUtil.getChildrenOfType(commands, LeafPsiElement::class.java)
        return Arrays.stream(leafs!!)
            .anyMatch { l -> l.elementType == LatexTypes.STAR }
    }

    private fun highestLevel(sections: Deque<LatexStructureViewCommandElement>): String {
        return sections.stream()
            .map { this.order(it) }
            .min { obj, anotherInteger -> obj.compareTo(anotherInteger) }
            .map { CommandMagic.sectionMarkers[it] }
            .orElse("\\section")
    }

    private fun pop(sections: Deque<LatexStructureViewCommandElement>) {
        sections.removeFirst()
    }

    private fun queue(
        child: LatexStructureViewCommandElement,
        sections: Deque<LatexStructureViewCommandElement>
    ) {
        sections.addFirst(child)
    }

    private fun current(sections: Deque<LatexStructureViewCommandElement>) = sections.peekFirst() ?: null

    private fun order(element: LatexStructureViewCommandElement) = order(element.commandName)

    private fun order(commands: LatexCommands) = order(commands.commandToken.text)

    private fun order(commandName: String) = CommandMagic.sectionMarkers.indexOf(commandName)
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.ide.structureView.StructureViewModel
import com.intellij.ide.structureView.TreeBasedStructureViewBuilder
import com.intellij.lang.PsiStructureViewFactory
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.psi.StructurePsiChangeListener

/**
 * @author Hannah Schellekens
 */
class LatexStructureViewFactory : PsiStructureViewFactory {

    override fun getStructureViewBuilder(psiFile: PsiFile) = object : TreeBasedStructureViewBuilder() {

        override fun createStructureViewModel(editor: Editor?): StructureViewModel {
            val project = editor?.project ?: return LatexStructureViewModel(psiFile, editor)
            val manager = PsiManager.getInstance(project)
            manager.addPsiTreeChangeListener(StructurePsiChangeListener(project)) { }
            return LatexStructureViewModel(psiFile, editor)
        }
    }
}

package nl.hannahsten.texifyidea.structure.latex

import com.intellij.ide.structureView.StructureViewModel.ElementInfoProvider
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.structureView.TextEditorBasedStructureViewModel
import com.intellij.ide.util.treeView.smartTree.Sorter
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.filter.*

/**
 * @author Hannah Schellekens
 */
class LatexStructureViewModel(
    psiFile: PsiFile,
    editor: Editor?
) : TextEditorBasedStructureViewModel(editor, psiFile), ElementInfoProvider {

    companion object {

        private val sorterArray = arrayOf(Sorter.ALPHA_SORTER)
        private val filterArray = arrayOf(
            IncludesFilter(),
            SectionFilter(),
            CommandDefinitionFilter(),
            LabelFilter(),
            BibitemFilter()
        )
    }

    override fun getRoot() = LatexStructureViewElement(psiFile)

    override fun getSorters() = sorterArray

    override fun getFilters() = filterArray

    override fun isAlwaysShowsPlus(structureViewTreeElement: StructureViewTreeElement) = false

    override fun isAlwaysLeaf(structureViewTreeElement: StructureViewTreeElement) = false

    override fun getSuitableClasses(): Array<Class<LatexCommands>> {
        return arrayOf(
            LatexCommands::class.java
        )
    }
}

package nl.hannahsten.texifyidea.structure.latex

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation

/**
 * @author Hannah Schellekens
 */
class LatexSubParagraphPresentation(subParagraphCommand: LatexCommands) : EditableHintPresentation {

    private val subParagraphName: String
    private var hint = ""

    init {
        if (subParagraphCommand.commandToken.text != "\\subparagraph") {
            throw IllegalArgumentException("command is no \\subparagraph-command")
        }

        this.subParagraphName = subParagraphCommand.getRequiredParameters()[0]
    }

    override fun getPresentableText() = subParagraphName

    override fun getLocationString() = hint

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_SUBPARAGRAPH

    override fun setHint(hint: String) {
        this.hint = hint
    }
}

package nl.hannahsten.texifyidea.structure.latex

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation

/**
 * @author Hannah Schellekens
 */
class LatexSubSectionPresentation(sectionCommand: LatexCommands) : EditableHintPresentation {

    private val subSectionName: String
    private var hint = ""

    init {
        if (sectionCommand.commandToken.text != "\\subsection") {
            throw IllegalArgumentException("command is no \\subsection-command")
        }

        this.subSectionName = sectionCommand.getRequiredParameters()[0]
    }

    override fun getPresentableText() = subSectionName

    override fun getLocationString() = hint

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_SUBSECTION

    override fun setHint(hint: String) {
        this.hint = hint
    }
}

package nl.hannahsten.texifyidea.structure.latex

import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation

/**
 * @author Hannah Schellekens
 */
class LatexSubSubSectionPresentation(sectionCommand: LatexCommands) : EditableHintPresentation {

    private val subSubSectionName: String
    private var hint = ""

    init {
        if (sectionCommand.commandToken.text != "\\subsubsection") {
            throw IllegalArgumentException("command is no \\subsubsection-command")
        }

        this.subSubSectionName = sectionCommand.getRequiredParameters()[0]
    }

    override fun getPresentableText() = subSubSectionName

    override fun getLocationString() = hint

    override fun getIcon(b: Boolean) = TexifyIcons.DOT_SUBSUBSECTION

    override fun setHint(hint: String) {
        this.hint = hint
    }
}

package nl.hannahsten.texifyidea.structure.latex

import nl.hannahsten.texifyidea.util.toRoman
import java.util.*
import kotlin.math.max

/**
 * @author Hannah Schellekens
 */
class SectionNumbering(private val documentClass: DocumentClass) {

    /**
     * 0: Part
     * 1: Chapter
     * 2: Section
     * 3: Subsection
     * 4: Subsubsection
     * 5: Paragraph
     * 6: Subparagraph
     */
    private val counters = IntArray(7)

    fun increase(level: Int) {
        counters[level]++

        // Parts don't reset other counters.
        if (level == 0) {
            return
        }

        for (i in level + 1 until counters.size) {
            counters[i] = 0
        }
    }

    private fun getCounter(level: Int) = counters[level]

    fun setCounter(level: Int, amount: Int) {
        counters[level] = amount
    }

    fun addCounter(level: Int, amount: Int) {
        counters[level] += amount
    }

    fun getTitle(level: Int): String {
        // Parts
        if (level == 0) {
            return max(0, counters[0]).toRoman()
        }

        val sb = StringBuilder()
        var delimiter = ""

        for (i in documentClass.startIndex..level) {
            sb.append(delimiter)
            sb.append(getCounter(i))
            delimiter = "."
        }

        return sb.toString()
    }

    enum class DocumentClass(val startIndex: Int) {

        BOOK(1),
        ARTICLE(2);

        val className: String
            get() = name.lowercase(Locale.getDefault())

        companion object {

            fun getClassByName(name: String): DocumentClass {
                return if (BOOK.name == name) {
                    BOOK
                }
                else ARTICLE
            }
        }
    }
}

package nl.hannahsten.texifyidea.templates.macros

import com.intellij.codeInsight.template.*
import com.intellij.codeInsight.template.macro.MacroBase
import nl.hannahsten.texifyidea.templates.LatexContext
import nl.hannahsten.texifyidea.util.formatAsLabel

class LatexFormatAsLabelMacro : MacroBase("latexFormatAsLabel", "latexFormatAsLabel(String)") {

    override fun calculateResult(params: Array<out Expression>, context: ExpressionContext?, quick: Boolean): Result? {
        val text = getTextResult(params, context, true) ?: return null
        return TextResult(text.formatAsLabel())
    }

    override fun isAcceptableInContext(context: TemplateContextType?) = context is LatexContext
}

package nl.hannahsten.texifyidea.ui.remotelibraries

import com.intellij.openapi.ui.DialogWrapper

abstract class AddLibDialogWrapper(val displayName: String) : DialogWrapper(true)

package nl.hannahsten.texifyidea.ui.remotelibraries

import com.intellij.ui.treeStructure.Tree
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel

fun Tree.findLibraryNode(identifier: String): LibraryMutableTreeNode? {
    val model = model as DefaultTreeModel
    val root = model.root as? DefaultMutableTreeNode
    return root?.children()
        ?.asSequence()
        ?.firstOrNull { (it as LibraryMutableTreeNode).identifier == identifier } as? LibraryMutableTreeNode
}

package nl.hannahsten.texifyidea.ui.remotelibraries

import com.intellij.ide.util.treeView.NodeRenderer
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.content.ContentFactory
import com.intellij.ui.treeStructure.Tree
import nl.hannahsten.texifyidea.remotelibraries.RemoteLibraryManager
import nl.hannahsten.texifyidea.structure.bibtex.BibtexStructureViewEntryElement
import nl.hannahsten.texifyidea.structure.bibtex.BibtexStructureViewTagElement
import nl.hannahsten.texifyidea.util.TexifyDataKeys
import nl.hannahsten.texifyidea.util.isLatexProject
import javax.swing.tree.DefaultMutableTreeNode

/**
 * The remote libraries tool window shows an overview of all remote libraries a user has connected with.
 *
 * @author Abby Berkers
 */
class RemoteLibrariesToolWindowFactory : ToolWindowFactory {

    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        val librariesToolWindow = RemoteLibrariesToolWindowPanel(project)
        val content = ContentFactory.getInstance().createContent(librariesToolWindow, "", false)
        toolWindow.contentManager.addContent(content)
    }

    override fun isApplicable(project: Project) = project.isLatexProject()

    /**
     * The tool window panel that contains the toolbar and the actual window (which is [RemoteLibraryToolWindow]).
     */
    class RemoteLibrariesToolWindowPanel(val project: Project) : SimpleToolWindowPanel(true, false), DataProvider {

        private val toolWindow = RemoteLibraryToolWindow(project)

        init {
            toolbar = toolWindow.toolbar.apply {
                targetComponent = this@RemoteLibrariesToolWindowPanel
            }.component

            setContent(toolWindow.content)
        }

        override fun getData(dataId: String): Any? = toolWindow.getData(dataId)
    }

    /**
     * The UI elements of the tool window contents. Most actual UI elements are taken from the structure view, with the
     * aim of this tree looking the same as the one in the structure view.
     */
    class RemoteLibraryToolWindow(val project: Project) : DataProvider {

        private val actionManager: ActionManager = ActionManager.getInstance()

        val toolbar = actionManager.createActionToolbar(
            ActionPlaces.TOOLWINDOW_TOOLBAR_BAR,
            actionManager.getAction("texify.remotelibraries") as ActionGroup,
            true
        )

        val libraries = RemoteLibraryManager.getInstance().getLibraries().toMap().entries

        private val rootNode = DefaultMutableTreeNode().apply {
            // Add all the bib items for each library.
            libraries.forEach { library ->
                val libraryNode = LibraryMutableTreeNode(library.key, library.value.displayName).apply {
                    library.value.entries.forEach { entry ->
                        val entryElement = BibtexStructureViewEntryElement(entry)
                        val entryNode = DefaultMutableTreeNode(entryElement)
                        add(entryNode)

                        // Each bib item has tags that show information, e.g., the author.
                        entryElement.children.forEach {
                            entryNode.add(DefaultMutableTreeNode(it))
                        }
                    }
                }

                add(libraryNode)
            }
        }

        val tree = Tree(rootNode).apply {
            // The root is an invisible dummy so multiple libraries can be shown in the tree alongside one another
            // without a redundant (and annoying) root element.
            isRootVisible = false

            // Nodes in the tree of the structure view are rendered by NodeRenderer, which internally uses PresentationData
            // (a subclass of ItemPresentation) to render a cell. We use the same renderer and presentation data here
            // for consistency.
            cellRenderer = object : NodeRenderer() {
                // We cannot depend on the StructureView to resolve the presentation for us, so we have to manually point
                // the renderer to our custom PresentationData (which we can reuse from the structure view's elements).
                override fun getPresentation(node: Any?): ItemPresentation? =
                    when (node) {
                        is BibtexStructureViewEntryElement -> node.presentation
                        is BibtexStructureViewTagElement -> node.presentation
                        else -> super.getPresentation(node)
                    }
            }
        }

        val content = JBScrollPane(tree)

        override fun getData(dataId: String): Any? {
            return when {
                TexifyDataKeys.LIBRARY_TREE.`is`(dataId) -> tree
                TexifyDataKeys.LIBRARY_NAME.`is`(dataId) -> (tree.selectionPath?.getPathComponent(1) as? LibraryMutableTreeNode)?.toString()
                TexifyDataKeys.LIBRARY_IDENTIFIER.`is`(dataId) -> (tree.selectionPath?.getPathComponent(1) as? LibraryMutableTreeNode)?.identifier
                else -> null
            }
        }
    }
}

class LibraryMutableTreeNode(val identifier: String, val displayName: String) : DefaultMutableTreeNode(displayName)

package nl.hannahsten.texifyidea.ui.symbols

import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.util.formatAsFileName
import java.util.*

/**
 * Quickly creates a SymbolUiEntry from a command with the following consequences:
 *
 * - By default it generates only the command with slash, this can be changed by setting `generatedLatex`.
 * - The file path is "/nl/hannahsten/texifyidea/symbols/".
 * - The file name is either "math_ENUM-CONSTANT-LOWERCASE.png" for math commands, is
 *      "text_<ENUM-CONSTANT-LOWERCASE>.png" for regular commands, and
 *      "misc_<PACKAGE><FILENAME-SAFE-COMMAND>.png" for other commands. This can be changed by setting `customFileName`.
 * - The description is the command with a slash, can be changed by setting `customDescription`.
 * - Uses the command (in math mode if applicable) to generate the image, can be changed by setting
 *      `customImageLatex`.
 *
 * @author Hannah Schellekens
 */
class CommandUiEntry(
    override val command: LatexCommand,
    generatedLatex: String? = null,
    customFileName: String? = null,
    customDescription: String? = null,
    customImageLatex: String? = null
) : SymbolUiEntry {

    override val generatedLatex: String = generatedLatex ?: command.commandWithSlash

    override val fileName = customFileName ?: if (command.isMathMode) {
        "math_${command.identifier.formatAsFileName()}.png"
    }
    else "text_${command.identifier.formatAsFileName()}.png"

    override val imagePath = "/nl/hannahsten/texifyidea/symbols/$fileName"

    override val imageLatex = customImageLatex ?: command.commandWithSlash

    override val description = customDescription ?: (
        command.identifier
            .lowercase(Locale.getDefault())
            .replace("_", " ") + if (command.isMathMode) " (math)" else ""
        )

    override val isMathSymbol = command.isMathMode
}

package nl.hannahsten.texifyidea.ui.symbols

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexCommand

/**
 * @author Hannah Schellekens
 */
class DryUiEntry(
    override val description: String,
    override val generatedLatex: String,
    override val fileName: String,
    override val imageLatex: String,
    override val isMathSymbol: Boolean,
    override val dependency: LatexPackage = LatexPackage.DEFAULT
) : SymbolUiEntry {

    override val command: LatexCommand? = null

    override val imagePath = "/nl/hannahsten/texifyidea/symbols/$fileName"
}

package nl.hannahsten.texifyidea.ui.symbols

import nl.hannahsten.texifyidea.lang.commands.*
import nl.hannahsten.texifyidea.lang.commands.LatexArrowCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexColoneqCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexGenericMathCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexOperatorCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexGreekCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexDelimiterCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexEuroCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexGenericMathCommand.UNDERLINE
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexTextSymbolCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexTextcompCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexUncategorizedStmaryrdSymbols.*

/**
 * @author Hannah Schellekens
 */
object SymbolCategories {

    /**
     * Maps each category to the symbols that are in the category.
     *
     * The categories are ordered, as are the symbols per category.
     */
    val categories: Map<SymbolCategory, List<SymbolUiEntry>> = LinkedHashMap<SymbolCategory, List<SymbolUiEntry>>().apply {

        createCategory("Operators") {
            add(DryUiEntry(description = "plus sign", "+", "misc_plus.png", "+", true))
            add(DryUiEntry(description = "minus sign", "-", "misc_minus.png", "-", true))
            add(PLUS_MINUS)
            add(MINUS_PLUS)
            add(TIMES)
            add(DIV)
            add(ASTERISK)
            add(STAR)
            add(CIRCLE)
            add(BULLET)
            add(DIVIDEONTIMES)
            add(LTIMES)
            add(RTIMES)
            add(CDOT)
            add(DOT_PLUS)
            add(LEFT_THREE_TIMES)
            add(RIGHT_THREE_TIMES)
            add(DryUiEntry(description = "factorial", "!", "misc_factorial.png", "!", true))
            add(AMALGAMATION)
            add(CIRCLED_TIMES)
            add(CIRCLED_PLUS)
            add(CIRCLED_MINUS)
            add(CIRCLED_SLASH)
            add(CIRCLED_DOT)
            add(CIRCLED_CIRCLE)
            add(CIRCLED_DASH)
            add(CIRCLED_ASTERISK)
            add(BIG_CIRCLE)
            add(BOXED_DOT)
            add(BOXED_MINUS)
            add(BOXED_PLUS)
            add(BOXED_TIMES)
            add(DIAMOND)
            add(BIG_TRIANGLE_UP)
            add(TRIANGLE_LEFT)
            add(TRIANGLE_RIGHT)
            add(LHD)
            add(RHD)
            add(UN_LHD)
            add(UN_RHD)
            add(UNION)
            add(INTERSECTION)
            add(MULTISET_UNION)
            add(DOUBLE_UNION)
            add(DOUBLE_INTERSECTION)
            add(WREATH_PRODUCT)
            add(SET_MINUS)
            add(SMALL_SET_MINUS)
            add(SQUARE_CAP)
            add(SQUARE_CUP)
            add(NOT_SIGN)
            add(INVERSED_NOT_SIGN)
            add(WEDGE)
            add(VEE)
            add(WEDGE_BAR)
            add(VEE_BAR)
            add(DOUBLE_BAR_WEDGE)
            add(CURLY_WEDGE)
            add(CURLY_VEE)
            add(DAGGER)
            add(DOUBLE_DAGGER)
            add(INTERCALATE)
            add(N_ARY_INTERSECTION)
            add(N_ARY_UNION)
            add(N_ARY_UNION_WITH_PLUS)
            add(N_ARY_SQUARE_UNION)
            add(N_ARY_PRODUCT)
            add(N_ARY_COPRODUCT)
            add(BIG_WEDGE)
            add(BIG_VEE)
            add(BIG_CIRCLED_DOT)
            add(BIG_CIRCLED_PLUS)
            add(BIG_CIRCLED_TIMES)
            add(SUM)
            add(INTEGRAL)
            add(CONTOUR_INTEGRAL)
            add(DOUBLE_INTEGRAL)
            add(TRIPLE_INTEGRAL)
            add(QUADRUPLE_INTEGRAL)
            add(DOTS_INTEGRAL)
        }

        createCategory("Math functions") {
            add(INVERSE_COSINE)
            add(INVERSE_SINE)
            add(INVERSE_TANGENT)
            add(ARGUMENT)
            add(COSINE)
            add(HYPERBOLIC_COSINE)
            add(COTANGENT)
            add(HYPERBOLIC_COTANGENT)
            add(COSECANT)
            add(DEGREES)
            add(DERMINANT)
            add(DIMENSION)
            add(EXPONENTIAL)
            add(GREATEST_COMMON_DIVISOR)
            add(HOMOMORPHISM)
            add(INFINUM)
            add(KERNEL)
            add(BASE_2_LOGARITHM)
            add(LIMIT)
            add(LIMIT_INFERIOR)
            add(LIMIT_SUPERIOR)
            add(NATURAL_LOGARITHM)
            add(LOGARITHM)
            add(MAXIMUM)
            add(MINIMUM)
            add(PROBABILITY)
            add(INVERSE_LIMIT)
            add(SECANT)
            add(SINE)
            add(HYPERBOLIC_SINE)
            add(SUPREMUM)
            add(TANGENT)
            add(HBOLICTANGENT)
            add(LIMIT_SUPERIOR_VARIANT)
            add(LIMIT_INFERIOR_VARIANT)
            add(DIRECT_LIMIT_VARIANT)
            add(INVERSE_LIMIT_VARIANT)
        }

        createCategory("Relations") {
            add(BOWTIE)
            add(JOIN)
            add(PROPORTIONAL_TO)
            add(PROPORTIONAL_TO_SYMBOL)
            add(WASYSYM_PROPTO)
            add(MULTIMAP)
            add(PITCHFORK)
            add(THEREFORE)
            add(BECAUSE)
            add(DryUiEntry(description = "equals sign", "=", "misc_equals.png", "=", true))
            add(NOT_EQUAL)
            add(EQUIVALENT)
            add(APPROX)
            add(TILDE_OPERATOR)
            add(NOT_SIM)
            add(SIM_EQUALS)
            add(BACKWARDS_SIM_EQUALS)
            add(APPROX_EQUALS)
            add(CONG)
            add(NOT_CONG)
            add(COLON_EQUALSS)
            add(EQUALSS_COLON)
            add(COLON_EQUALS)
            add(EQUALS_COLON)
            add(COLON_APPROX)
            add(COLON_SIM)
            add(DOUBLE_COLON)
            add(DOUBLE_COLON_EQUALSS)
            add(EQUALSS_DOUBLE_COLON)
            add(DOUBLE_COLON_EQUALS)
            add(EQUALS_DOUBLE_COLON)
            add(DOUBLE_COLON_APPROX)
            add(DOUBLE_COLON_SIM)
            add(CIRCLE_EQUALS)
            add(TRIANGLE_EQUALS)
            add(EQUALS_CIRCLE)
            add(BUMP_EQUALS)
            add(DOUBLE_BUMP_EQUALS)
            add(DOT_EQUALS_DOT)
            add(RISING_DOTS_EQUALS)
            add(FALLING_DOTS_EQUALS)
            add(DOT_EQUALS)
            add(SMILE)
            add(FROWN)
            add(ASYMP)
            add(SMALL_FROWN)
            add(SMALL_SMILE)
            add(BETWEEN)
            add(PRECEDES)
            add(SUCCEEDS)
            add(NOT_PRECEEDS)
            add(NOT_SUCCEEDS)
            add(PRECEDES_OR_EQUAL)
            add(SUCCEEDS_OR_EQUALS)
            add(NOT_PRECEDES_OR_EQUALS)
            add(NOT_SUCCEEDS_OR_EQUALS)
            add(CURLY_PRECEDES_OR_EQUALS)
            add(CURLY_SUCCEEDS_OR_EQUALS)
            add(CURLY_EQUALS_PRECEDES)
            add(CURLY_EQUALS_SUCCEEDS)
            add(PRECEDES_SIM)
            add(SUCCEEDS_SIM)
            add(PRECEDES_NOT_SIM)
            add(SUCCEEDS_NOT_SIM)
            add(PRECEDES_APPROX)
            add(SUCCEEDS_APPROX)
            add(PRECEDES_NOT_APPROX)
            add(SUCCEEDS_NOT_APPROX)
            add(PERPENDICULAR)
            add(RIGHT_TACK)
            add(LEFT_TACK)
            add(NOT_RIGHT_TACK)
            add(FORCES)
            add(TRIPLE_RIGHT_TACK)
            add(MODELS)
            add(VERTICAL_DOUBLE_DASH_RIGHT)
            add(NOT_VERTICAL_DOUBLE_DASH_RIGHT)
            add(NOT_DOUBLE_VERTICAL_DOUBLE_DASH_RIGHT)
            add(MID)
            add(NOT_MID)
            add(PARALLEL)
            add(NOT_PARALLEL)
            add(MID_SHORT)
            add(NOT_MID_SHORT)
            add(NOT_PARALLEL_SHORT)
            add(DryUiEntry(description = "less than", "<", "misc_lesser.png", "<", true))
            add(DryUiEntry(description = "greater than", ">", "misc_greater.png", ">", true))
            add(NOT_LESS_THAN)
            add(NOT_GREATER_THAN)
            add(LESS_THAN_DOT)
            add(GREATER_THAN_DOT)
            add(DOUBLE_LESS_THAN)
            add(DOUBLE_GREATER_THAN)
            add(LESS_LESS_LESS)
            add(GREATER_GREATER_GREATER)
            add(LESS_THAN_EQUAL)
            add(GREATER_THAN_EQUAL)
            add(LESS_THAN_NOT_EQUAL)
            add(GREATER_THAN_NOT_EQUAL)
            add(NOT_LESS_THAN)
            add(NOT_GREATER_THAN)
            add(LESS_THAN_EQUALL)
            add(GREATER_THAN_EQUALL)
            add(LESS_NOT_EQUAL)
            add(GREATER_NOT_EQUAL)
            add(LESS_THAN_VERTICAL_NOT_EQUALS)
            add(GREATER_THAN_VERTICAL_NOT_EQUALS)
            add(NOT_LESS_THAN_EQUALL)
            add(NOT_GREATER_THAN_EQUALL)
            add(LESS_THAN_EQUALS_SLANT)
            add(GREATER_THAN_EQUALS_SLANT)
            add(NOT_LESS_THAN_EQUALS_SLANT)
            add(NOT_GREATER_THAN_EQUALS_SLANT)
            add(EQUALS_SLANT_LESS_THAN)
            add(EQUALS_SLANT_GREATER_THAN)
            add(LESS_GREATER)
            add(GREATER_LESS)
            add(LESS_EQUALS_GREATER)
            add(GREATER_EQUALS_LESSER)
            add(LESS_EQUALSS_GREATER)
            add(GREATER_EQUALSS_LESSER)
            add(LESS_SIM)
            add(GREATER_SIM)
            add(LESS_NOT_SIM)
            add(GREATER_NOT_SIM)
            add(LESS_APPROX)
            add(GREATER_APPROX)
            add(LESS_NOT_APPROX)
            add(GREATER_NOT_APPROX)
            add(TRIANGLE_LEFT_VARIATION)
            add(TRIANGLE_RIGHT_VARIATION)
            add(NOT_TRIANGLE_LEFT)
            add(NOT_TRIANGLE_RIGHT)
            add(TRIANGLE_LEFT_EQUALS)
            add(TRIANGLE_RIGHT_EQUALS)
            add(TRIANGLE_LEFT_EQUALS_SLANT)
            add(TRIANGLE_RIGHT_EQUALS_SLANT)
            add(NOT_TRIANGLE_LEFT_EQUALS)
            add(NOT_TRIANGLE_RIGHT_EQUALS)
            add(NOT_TRIANGLE_LEFT_EQUALS_SLANT)
            add(NOT_TRIANGLE_RIGHT_SLANT)
            add(BLACK_TRIANGLE_LEFT)
            add(BLACK_TRIANGLE_RIGHT)
            add(CONTAIN_AS_MEMBER)
            add(ELEMENT_OF)
            add(NOT_ELEMENT_OF)
            add(SUBSET)
            add(SUPERSET)
            add(SUBSET_EQUALS)
            add(SUPERSET_EQUALS)
            add(SUBSET_NOT_EQUALS)
            add(SUPERSET_NOT_EQUALS)
            add(SUBSET_NOT_EQUALS_VARIATION)
            add(SUPERSET_NOT_EQUALS_VARIATION)
            add(NOT_SUBSET_EQUALS)
            add(NOT_SUPERSET_EQUALS)
            add(SUBSET_EQUALSS)
            add(SUPERSET_EQUALSS)
            add(SUBSET_NOT_EQUALSS)
            add(SUPERSET_NOT_EQUALSS)
            add(NOT_SUBSET_EQUALSS)
            add(NOT_SUPERSET_EQUALSS)
            add(SUBSET_PLUS)
            add(SUBSET_PLUS_EQUALS)
            add(SUPERSET_PLUS)
            add(SUPERSET_PLUS_EQUALS)
            add(REVERSED_EPSILON)
            add(DOUBLE_SUBSET)
            add(DOUBLE_SUPERSET)
            add(SQUARE_SUBSET)
            add(SQUARE_SUPERSET)
            add(SQUARE_SUBSET_EQUALS)
            add(SQUARE_SUPERSET_EQUALS)
            add(IN_PLUS)
            add(REVERSED_IN_PLUS)
        }

        createCategory("Arrows") {
            add(LEFT_ARROW)
            add(LEFT_AND_RIGHT_ARROW)
            add(RIGHT_ARROW)
            add(LONG_LEFT_ARROW)
            add(LONG_LEFT_AND_RIGHT_ARROW)
            add(LONG_RIGHT_ARROW)
            add(SHORT_LEFT_ARROW)
            add(SHORT_RIGHT_ARROW)
            add(UP_ARROW)
            add(UP_AND_DOWN_ARROW)
            add(DOWN_ARROW)
            add(SHORT_DOWN_ARROW)
            add(SHORT_UP_ARROW)
            add(NORTH_NORTH_EAST_ARROW)
            add(NORTH_EAST_ARROW)
            add(SOUTH_EAST_ARROW)
            add(SOUNT_SOUTH_EAST_ARROW)
            add(SOUTH_SOUTH_WEST_ARROW)
            add(SOUTH_WEST_ARROW)
            add(NORTH_WEST_ARROW)
            add(NORTH_NORTH_WEST_ARROW)
            add(MAPS_TO)
            add(MAPS_FROM)
            add(MAPS_TO_DOUBLE_ARROW)
            add(MAPS_FROM_DOUBLE_ARROW)
            add(LONG_MAPS_TO_DOUBLE_ARROW)
            add(LONG_MAPS_FROM_DOUBLE_ARROW)
            add(LONG_MAPS_TO)
            add(LONG_MAPS_FROM)
            add(NOT_LEFT_ARROW)
            add(NOT_LEFT_AND_RIGHT_ARROW)
            add(NOT_RIGHT_ARROW)
            add(LEFT_ARROW_WITH_HOOK)
            add(RIGHT_ARROW_WITH_HOOK)
            add(TWO_HEADED_LEFT_ARROW)
            add(TWO_HEADED_RIGHT_ARROW)
            add(LEFT_ARROW_WITH_TAIL)
            add(RIGHT_ARROW_WITH_TAIL)
            add(LEFT_DOUBLE_ARROW)
            add(LEFT_AND_RIGHT_DOUBLE_ARROW)
            add(RIGHT_DOUBLE_ARROW)
            add(UP_DOUBLE_ARROW)
            add(UP_AND_DOWN_DOUBLE_ARROW)
            add(DOWN_DOUBLE_ARROW)
            add(LONG_LEFT_DOUBLE_ARROW)
            add(LONG_LEFT_AND_RIGHT_DOUBLE_ARROW)
            add(LONG_RIGHT_ARROW)
            add(NOT_LEFT_DOUBLE_ARROW)
            add(NOT_LEFT_AND_RIGHT_DOUBLE_ARROW)
            add(NOT_RIGHT_DOUBLE_ARROW)
            add(LEFT_ARROW_PAIR)
            add(LEFT_AND_RIGHT_ARROW_PAIR)
            add(RIGHT_AND_LEFT_ARROW_PAIR)
            add(RIGHT_ARROW_PAIR)
            add(UP_ARROW_PAIR)
            add(DOWN_ARROW_PAIR)
            add(ANTI_CLOCKWISE_CIRCLE_ARROW)
            add(CLOCKWISE_CIRCLE_ARROW)
            add(ANTI_CLOCKWISE_SEMICIRCLE_ARROW_TOP)
            add(CLOCKWISE_SEMICIRCLE_ARROW_TOP)
            add(UP_ARROW_WITH_LEFT_TIP)
            add(UP_ARROW_WITH_RIGHT_TIP)
            add(LEFT_ARROW_WITH_LOOP)
            add(RIGHT_ARROW_WITH_LOOP)
            add(DASHED_LEFT_ARROW)
            add(DASHED_RIGHT_ARROW)
            add(LEFT_AND_RIGHT_WAVE_ARROW)
            add(RIGHT_WAVE_ARROW)
            add(LEFT_TRIPLE_ARROW)
            add(LEFT_HARPOON_DOWN)
            add(RIGHT_HARPOON_DOWN)
            add(LEFT_HARPOON_UP)
            add(RIGHT_HARPOON_UP)
            add(RIGHT_AND_LEFT_HARPOONS)
            add(LEFT_AND_RIGHT_HARPOONS)
            add(UP_HARPOON_LEFT)
            add(DOWN_HARPOON_LEFT)
            add(UP_HARPOON_RIGHT)
            add(DOWN_HARPOON_RIGHT)
            add(TEXT_LEFT_ARROW)
            add(TEXT_RIGHT_ARROW)
            add(TEXT_UP_ARROW)
            add(TEXT_DOWN_ARROW)
            add(LEFT_OPEN_HEADED_ARROW)
            add(LEFT_AND_RIGHT_OPEN_HEADED_ARROW)
            add(RIGHT_OPEN_HEADED_ARROW)
            add(LEFT_AND_RIGHT_ARROW_EQUALS)
            add(LIGHTNING)
        }

        createCategory("Delimiters") {
            // Place the enclosing delimiters at the front:
            // 1. Makes the symbol overview less cluttered.
            // 2. Encourages usage of \left\right over standalone symbols.
            add(DryUiEntry(description = "brackets", "\\left[ <caret> \\right]", "misc_brackets_pair.png", "\\left[...\\right]", true))
            addLeftRight(LEFT_BRACKET_BOLD, RIGHT_BRACKET_BOLD, "bold brackets", "misc_bold_brackets_pair.png")
            addLeftRight(LEFT_ANGLE_BRACKET, RIGHT_ANGLE_BRACKET, "angular brackets", "misc_angular_brackets_pair.png")
            add(DryUiEntry(description = "absolute value", "\\left| <caret> \\right|", "misc_absolute_value_pair.png", "\\left|...\\right|", true))
            addLeftRight(DOUBLE_VERTICAL_LINE, DOUBLE_VERTICAL_LINE, "length", "misc_length_pair.png")
            addLeftRight(LEFT_CEIL, RIGHT_CEIL, "ceiling", "misc_ceiling_pair.png")
            addLeftRight(LEFT_CEIL_BOLD, RIGHT_CEIL_BOLD, "bold ceiling", "misc_bold_ceiling_pair.png", requireLeftRight = false)
            addLeftRight(LEFT_FLOOR, RIGHT_FLOOR, "floor", "misc_floor_pair.png")
            addLeftRight(LEFT_FLOOR_BOLD, RIGHT_FLOOR_BOLD, "bold floor", "misc_bold_floor_pair.png", requireLeftRight = false)
            add(DryUiEntry(description = "parentheses", "\\left( <caret> \\right)", "misc_parentheses_pair.png", "\\left(...\\right)", true))
            addLeftRight(LEFT_PARENTHESIS_BOLD, RIGHT_PARENTHESIS_BOLD, "bold parentheses", "misc_bold_parentheses_pair.png", requireLeftRight = false)
            add(DryUiEntry(description = "braces", "\\left\\{ <caret> \\right\\}", "misc_braces_pair.png", "\\left\\{...\\right\\}", true))
            addLeftRight(LEFT_MOUSTACHE, RIGHT_MOUSTACHE, "moustaches", "misc_moustaches_pair.png")
            addLeftRight(LEFT_GROUP, RIGHT_GROUP, "group", "misc_group_pair.png")
            addLeftRight(BRACEVERT, BRACEVERT, "bracevert", "misc_bracevert_pair.png")
            addLeftRight(LEFT_BAG, RIGHT_BAG, "bag", "misc_bag_pair.png", requireLeftRight = false)
            addLeftRight(LEFT_BAG_BOLD, RIGHT_BAG_BOLD, "bold bag", "misc_bold_bag_pair.png", requireLeftRight = false)

            // Single delimiters.
            add(DryUiEntry(description = "left bracket", "[", "misc_left_bracket.png", "[", true))
            add(DryUiEntry(description = "right bracket", "]", "misc_right_bracket.png", "]", true))
            add(LEFT_BRACKET_BOLD)
            add(RIGHT_BRACKET_BOLD)
            add(LEFT_ANGLE_BRACKET)
            add(RIGHT_ANGLE_BRACKET)
            add(DryUiEntry(description = "vertical line", "|", "misc_vertical_line.png", "|", true))
            add(DOUBLE_VERTICAL_LINE)
            add(LEFT_CEIL)
            add(RIGHT_CEIL)
            add(LEFT_CEIL_BOLD)
            add(RIGHT_CEIL_BOLD)
            add(LEFT_FLOOR)
            add(RIGHT_FLOOR)
            add(LEFT_FLOOR_BOLD)
            add(RIGHT_FLOOR_BOLD)
            add(DryUiEntry(description = "left parenthesis", "(", "misc_left_parenthesis.png", "(", true))
            add(DryUiEntry(description = "right parenthesis", ")", "misc_right_parenthesis.png", ")", true))
            add(LEFT_PARENTHESIS_BOLD)
            add(RIGHT_PARENTHESIS_BOLD)
            add(DryUiEntry(description = "left brace", "\\{", "misc_left_brace.png", "\\{", true))
            add(DryUiEntry(description = "right brace", "\\}", "misc_right_brace.png", "\\}", true))
            add(DryUiEntry(description = "forward slash", "/", "misc_forward_slash.png", "/", true))
            add(BACKSLASH)
            add(LEFT_MOUSTACHE, latex = "\\left\\lmoustache", image = "\\left\\lmoustache\\right.")
            add(RIGHT_MOUSTACHE, latex = "\\right\\rmoustache", image = "\\left.\\right\\rmoustache")
            add(LEFT_GROUP, latex = "\\left\\lgroup", image = "\\left\\lgroup\\right.")
            add(RIGHT_GROUP, latex = "\\right\\rgroup", image = "\\left.\\right\\rgroup")
            add(BRACEVERT)
            add(LEFT_BAG)
            add(RIGHT_BAG)
            add(LEFT_BAG_BOLD)
            add(RIGHT_BAG_BOLD)
            add(UPPER_LEFT_CORNER)
            add(UPPER_RIGHT_CORNER)
            add(LOWER_LEFT_CORNER)
            add(LOWER_RIGHT_CORNER)
        }

        createCategory("Greek") {
            add(ALPHA)
            add(BETA)
            add(GAMMA)
            add(DELTA)
            add(BETTER_LOOKING_EPSILON)
            add(EPSILON)
            add(ZETA)
            add(ETA)
            add(THETA)
            add(THETA_VARIANT)
            add(IOTA)
            add(KAPPA)
            add(LAMBDA)
            add(MU)
            add(NU)
            add(XI)
            add(DryUiEntry(description = "omicron", "o", "misc_omicron.png", "o", true))
            add(PI)
            add(PI_VARIANT)
            add(RHO)
            add(RHO_VARIANT)
            add(SIGMA)
            add(FINAL_SIGMA)
            add(TAU)
            add(UPSILON)
            add(PHI_STRAIGHT)
            add(PHI_CURLY)
            add(CHI)
            add(PSI)
            add(OMEGA)
            add(DryUiEntry(description = "capital alpha", "A", "misc_capital_alpha.png", "A", true))
            add(DryUiEntry(description = "capital beta", "B", "misc_capital_beta.png", "B", true))
            add(CAPITAL_GAMMA)
            add(CAPITAL_GAMMA_VARIANT)
            add(CAPITAL_DELTA)
            add(CAPITAL_DELTA_VARIANT)
            add(DryUiEntry(description = "capital epsilon", "E", "misc_capital_epsilon.png", "E", true))
            add(DryUiEntry(description = "capital zeta", "Z", "misc_capital_zeta.png", "Z", true))
            add(DryUiEntry(description = "capital eta", "Z", "misc_capital_eta.png", "H", true))
            add(CAPITAL_THETA)
            add(CAPITAL_THETA_VARIANT)
            add(DryUiEntry(description = "capital iota", "I", "misc_capital_iota.png", "I", true))
            add(DryUiEntry(description = "capital kappa", "K", "misc_capital_kappa.png", "K", true))
            add(CAPITAL_LAMBDA)
            add(CAPITAL_LAMBDA_VARIANT)
            add(DryUiEntry(description = "capital mu", "M", "misc_capital_mu.png", "M", true))
            add(DryUiEntry(description = "capital nu", "M", "misc_capital_nu.png", "N", true))
            add(CAPITAL_XI)
            add(CAPITAL_XI_VARIANT)
            add(DryUiEntry(description = "capital omicron", "O", "misc_capital_omicron.png", "O", true))
            add(CAPITAL_PI)
            add(CAPITAL_PI_VARIANT)
            add(DryUiEntry(description = "capital rho", "P", "misc_capital_rho.png", "P", true))
            add(CAPITAL_SIGMA)
            add(CAPITAL_SIGMA_VARIANT)
            add(DryUiEntry(description = "capital tau", "T", "misc_capital_tau.png", "T", true))
            add(CAPITAL_UPSILON)
            add(CAPITAL_UPSILON_VARIANT)
            add(CAPITAL_PHI)
            add(CAPITAL_PHI_VARIANT)
            add(DryUiEntry(description = "capital chi", "X", "misc_capital_chi.png", "X", true))
            add(CAPITAL_PSI)
            add(CAPITAL_PSI_VARIANT)
            add(CAPITAL_OMEGA)
            add(CAPITAL_OMEGA_VARIANT)
        }

        createCategory("Misc. math") {
            add(ACUTE, latex = "\\acute{<caret>}", image = "\\acute{a}")
            add(GRAVE, latex = "\\grave{<caret>}", image = "\\grave{a}")
            add(CHECK, latex = "\\check{<caret>}", image = "\\check{a}")
            add(HAT, latex = "\\hat{<caret>}", image = "\\hat{a}")
            add(TILDE, latex = "\\tilde{<caret>}", image = "\\tilde{a}")
            add(BAR, latex = "\\bar{<caret>}", image = "\\bar{a}")
            add(VECTOR, latex = "\\vec{<caret>}", image = "\\vec{a}")
            add(BREVE, latex = "\\breve{<caret>}", image = "\\breve{a}")
            add(DOT, latex = "\\dot{<caret>}", image = "\\dot{a}")
            add(DOUBLE_DOTS, latex = "\\ddot{<caret>}", image = "\\ddot{a}")
            add(TRIPLE_DOTS, latex = "\\dddot{<caret>}", image = "\\dddot{a}")
            add(QUADRUPLE_DOTS, latex = "\\ddddot{<caret>}", image = "\\ddddot{a}")
            add(MATHRING, latex = "\\mathring{<caret>}", image = "\\mathring{a}")
            add(WIDE_TILDE, latex = "\\widetilde{<caret>}", image = "\\widetilde{abc}")
            add(WIDE_HAT, latex = "\\widehat{<caret>}", image = "\\widehat{abc}")
            add(OVER_LEFT_ARROW, latex = "\\overleftarrow{<caret>}", image = "\\overleftarrow{abc}")
            add(OVER_RIGHT_ARROW, latex = "\\overrightarrow{<caret>}", image = "\\overrightarrow{abc}")
            add(OVERLINE, latex = "\\overline{<caret>}", image = "\\overline{abc}")
            add(UNDERLINE, latex = "\\underline{<caret>}", image = "\\underline{abc}")
            add(OVERBRACE, latex = "\\overbrace{<caret>}", image = "\\overbrace{abc}")
            add(UNDERBRACE, latex = "\\underbrace{<caret>}", image = "\\underbrace{abc}")
            add(OVER_LEFT_RIGHT_ARROW, latex = "\\overleftrightarrow{<caret>}", image = "\\overleftrightarrow{abc}")
            add(UNDER_LEFT_RIGHT_ARROW, latex = "\\underleftrightarrow{<caret>}", image = "\\underleftrightarrow{abc}")
            add(UNDER_LEFT_ARROW, latex = "\\underleftarrow{<caret>}", image = "\\underleftarrow{abc}")
            add(UNDER_RIGHT_ARROW, latex = "\\underrightarrow{<caret>}", image = "\\underrightarrow{abc}")
            add(ABOVE_LEFT_ARROW, latex = "\\xleftarrow{<caret>}", image = "\\xleftarrow{abc}")
            add(ABOVE_RIGHT_ARROW, latex = "\\xrightarrow{<caret>}", image = "\\xrightarrow{abc}")
            add(STACKED_RELATION, latex = "\\stackrel{<caret>}{}", image = "\\stackrel{abc}{=}")
            add(SQUARE_ROOT, latex = "\\sqrt{<caret>}", image = "\\sqrt{x}")
            add(MIDDLE_DOT)
            add(COLON)
            add(LDOTP)
            add(VERTICAL_DOTS)
            add(CENTER_DOTS)
            add(DOWN_RIGHT_DIAGONAL_DOTS)
            add(LOWER_DOTS)
            add(INFINITY)
            add(PRIME)
            add(BACKSLASH)
            add(DIAGONAL_DOWN)
            add(DIAGONAL_UP)
            add(SQUARE_ROOT_SURD)
            add(EMPTY_SET)
            add(NOTHING)
            add(SHARP)
            add(FLAT)
            add(NATURAL)
            add(ANGLE)
            add(SPHERICAL_ANGLE)
            add(MEASURED_ANGLE)
            add(BOX)
            add(SQUARE)
            add(TRIANGLE_UP)
            add(TRIANGLE_VARIANT)
            add(TRIANGLE_DOWN)
            add(AMSSYMB_DIAMOND)
            add(DIAMOND_SUIT)
            add(LOZENGE)
            add(HEART_SUIT)
            add(BLACK_SQUARE)
            add(BLACK_TRIANGLE)
            add(BLACK_TRIANGLE_DOWN)
            add(BLACK_LOZENGE)
            add(BIG_STAR)
            add(SPADE_SUIT)
            add(CLUB_SUIT)
            add(FOR_ALL)
            add(EXISTS)
            add(NOT_EXISTS)
            add(INVERSE_F)
            add(MIRRORED_G)
            add(IMAGINARY)
            add(REAL)
            add(ALEPH)
            add(SCRIPT_CAPITAL_P)
            add(H_SLASH)
            add(H_BAR)
            add(DOTLESS_I)
            add(DOTLESS_J)
            add(BLACKBOARD_BOLD_K)
            add(SCRIPT_SMALL_L)
            add(CIRCLED_R)
            add(CIRCLED_S)
            add(O_CIRCLE)
            add(LOG_OF)
            add(BOT)
            add(TOP)
            add(PARTIAL)
            add(NABLA)
            add(ETH)
            add(REVERSED_OHM)
        }

        createCategory("Text") {
            add(ELLIPSIS)
            add(TEXT_TILDE_LOW)
            add(TEXT_ASCII_TILDE)
            add(TEXT_ASCII_CIRCUMFLEX)
            add(TEXT_ASCII_DIERESIS)
            add(TEXT_ASCII_ACUTE)
            add(TEXT_DOUBLE_ACUTE)
            add(TEXT_ASCII_GRAVE)
            add(TEXT_DOUBLE_GRAVE)
            add(TEXT_ASCII_BREVE)
            add(TEXT_ASCII_CARON)
            add(TEXT_LEFT_QUOTE)
            add(TEXT_RIGHT_QUOTE)
            add(TEXT_LEFT_DOUBLE_QUOTE)
            add(TEXT_RIGHT_DOUBLE_QUOTE)
            add(TEXT_QUOTE_STRAIGHT_BASE)
            add(TEXT_DOUBLE_QUOTE_STRAIGHT_BASE)
            add(TEXT_QUOTE_SINGLE)
            add(TEXT_DOUBLE_HYPHEN)
            add(TEXT_DOUBLE_HYPHEN_CHARACTER)
            add(TEXT_CENTERED_ASTERISK)
            add(TEXT_CENTERED_PERIOD)
            add(TEXT_UPSIDE_DOWN_QUESTION_MARK)
            add(TEXT_INTERROBANG)
            add(TEXT_UPSIDE_DOWN_INTERROBANG)
            add(TEXT_UPSIDE_DOWN_EXCLAMATION)
            add(TEXT_EN_DASH)
            add(TEXT_EM_DASH)
            add(TEXT_THREE_QUARTERS_EM_DASH)
            add(TEXT_VISIBLE_SPACE)
            add(UNDERSCORE)
            add(TEXT_CURRENCY)
            add(TEXT_CENT)
            add(TEXT_CENT_OLD_STYLE)
            add(TEXT_BAHT)
            add(TEXT_GUARANI)
            add(TEXT_WON)
            add(TEXT_DOLLAR)
            add(TEXT_DOLLAR_OLD_STYLE)
            add(TEXT_LIRA)
            add(TEXT_YEN)
            add(TEXT_DONG)
            add(TEXT_NAIRA)
            add(TEXT_COLON_MONETARY)
            add(TEXT_PESO)
            add(POUNDS)
            add(TEXT_FLORIN)
            add(EURO_AMOUNT, latex = "\\EUR{<caret>}", image = "\\EUR{\\ldots}")
            add(OFFICIAL_EURO)
            add(EURO_WIDE)
            add(EURO_NARROW)
            add(EURO)
            add(TEXT_EURO)
            add(CIRCLED_TEXT, latex = "\\textcircled{<caret>}", image = "\\textcircled{a}")
            add(TEXT_COPYRIGHT)
            add(TEXT_COPYLEFT)
            add(TEXT_REGISTERED)
            add(TEXT_TRADEMARK)
            add(TEXT_SERVICE_MARK)
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{0}", image = "\\oldstylenums{0}", fileName = "text_oldstylenums_0.png", description = "old style enum 0")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{1}", image = "\\oldstylenums{1}", fileName = "text_oldstylenums_1.png", description = "old style enum 1")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{2}", image = "\\oldstylenums{2}", fileName = "text_oldstylenums_2.png", description = "old style enum 2")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{3}", image = "\\oldstylenums{3}", fileName = "text_oldstylenums_3.png", description = "old style enum 3")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{4}", image = "\\oldstylenums{4}", fileName = "text_oldstylenums_4.png", description = "old style enum 4")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{5}", image = "\\oldstylenums{5}", fileName = "text_oldstylenums_5.png", description = "old style enum 5")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{6}", image = "\\oldstylenums{6}", fileName = "text_oldstylenums_6.png", description = "old style enum 6")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{7}", image = "\\oldstylenums{7}", fileName = "text_oldstylenums_7.png", description = "old style enum 7")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{8}", image = "\\oldstylenums{8}", fileName = "text_oldstylenums_8.png", description = "old style enum 8")
            add(OLDSTYLEENUMS, latex = "\\oldstylenums{9}", image = "\\oldstylenums{9}", fileName = "text_oldstylenums_9.png", description = "old style enum 9")
            add(TEXT_ONE_HALF)
            add(TEXT_ONE_QUARTER)
            add(TEXT_THREE_QUARTERS)
            add(TEXT_ONE_SUPERIOR)
            add(TEXT_TWO_SUPERIOR)
            add(TEXT_THREE_SUPERIOR)
            add(TEXT_NUMERO)
            add(TEXT_PER_MILLE)
            add(TEXT_PER_TEN_THOUSAND)
            add(TEXT_DISCOUNT)
            add(TEXT_BLANK)
            add(TEXT_RECIPE)
            add(TEXT_ESTIMATED)
            add(TEXT_REFERENCE_MARK)
            add(TEXT_MUSICAL_QUARTER_NOTE)
            add(TEXT_DAGGER)
            add(TEXT_DOUBLE_DAGGER)
            add(SECTION_SIGN)
            add(DOLLAR_SIGN)
            add(TEXT_PILCROW)
            add(CUT_RIGHT_ON_LINE)
            add(CUT_LINE)
            add(CUT_LEFT_ON_LINE)
            add(RIGHT_SCISSORS)
            add(CUT_LINE_MIDDLE)
            add(LEFT_SCISSORS)
            add(CHECKED_BOX)
            add(EMPTY_BOX)
            add(CROSSED_BOX)
            add(TEXT_BIG_CIRCLE)
            add(TEXT_OPEN_BULLET)
            add(TEXT_BULLET)
            add(CHECKMARK)
            add(MALTESE)
            add(TEXT_ORDINAL_INDICATOR_FEMININE)
            add(TEXT_ORDINAL_INDICATOR_MASCULINE)
            add(TEXT_BORN)
            add(TEXT_DIVORCED)
            add(TEXT_DIED)
            add(TEXT_MARRIED)
            add(TEXT_LEAF)
            add(TEXT_DEGREE)
            add(TEXT_OHM)
            add(TEXT_UPSIDE_DOWN_OHM)
            add(TEXT_MU)
            add(TEXT_BACKSLASH)
            add(TEXT_BAR)
            add(TEXT_BROKEN_BAR)
            add(TEXT_DOUBLE_BAR)
            add(TEXT_FRACTION_SOLIDUS)
            add(TEXT_LEFT_ANGLE_BRACKET)
            add(TEXT_RIGHT_ANGLE_BRACKET)
            add(TEXT_LEFT_DOUBLE_BRACKET)
            add(TEXT_RIGHT_DOUBLE_BRACKET)
            add(TEXT_LEFT_QUILL)
            add(TEXT_RIGHT_QUILL)
            add(TEXT_LEFT_NOT)
            add(TEXT_MINUS)
            add(TEXT_PLUS_MINUS)
            add(TEXT_LESS_THAN)
            add(TEXT_GREATER_THAN)
            add(TEXT_SURD)
            add(TEXT_TIMES)
            add(TEXT_DIVIDE)
        }

        createCategory("Misc. symbols") {
            // STMARYRD
            add(Y_UP)
            add(Y_RIGHT)
            add(Y_DOWN)
            add(Y_LEFT)
            add(BIN_AMPERSAND)
            add(BIN_REVERSED_AMPERSAND)
            add(O_BAR)
            add(BOX_BAR)
            add(BOX_CIRCLE)
            add(BOX_BOX)
            add(BOXED_DOT_STMARYRD)
            add(BOX_ASTERISK)
            add(BOX_BSLASH)
            add(BOX_EMPTY)
            add(BOX_SLASH)
            add(BIG_BOX)
            add(CURLY_VEE_VARIANT)
            add(BIG_CURLY_VEE)
            add(CURLY_WEDGE_VARIANT)
            add(BIG_CURLY_WEDGE_VARIANT)
            add(CURLY_VEE_DOWN_ARROW)
            add(CURLY_VEE_UP_ARROW)
            add(CURLY_WEDGE_DOWN_ARROW)
            add(CURLY_WEDGE_UP_ARROW)
            add(BLACKBOARD_BACKSLASH)
            add(BLACKBOARD_SLASH)
            add(FAT_BACKSLASH)
            add(FAT_SLASH)
            add(FAT_SEMICOLON)
            add(BIG_PARALLEL)
            add(INTERLEAVE)
            add(BIG_INTERLEAVE)
            add(TALL_OBLONG)
            add(LEFT_SLICE)
            add(RIGHT_SLICE)
            add(MERGE)
            add(MOO)
            add(CAP_PLUS)
            add(BIG_CAP_PLUS)
            add(O_HORIZONTAL_DASH)
            add(OBLONG)
            add(O_WITH_VERTICAL_LINE)
            add(O_WITH_BACKSLASH)
            add(O_WITH_GREATER_THAN)
            add(O_WITH_LESS_THAN)
            add(O_WITH_VEE)
            add(O_WITH_WEDGE)
            add(BIG_CIRCLE_VARIANT)
            add(O_WITH_STAR_VARIANT)
            add(O_WITH_VERTICAL_BAR_VARIANT)
            add(O_WITH_BACKSLASH_VARIANT)
            add(O_WITH_CIRCLE_VARIANT)
            add(O_WITH_DOT_VARIANT)
            add(O_WITH_GREATER_THAN_VARIANT)
            add(O_WITH_LESS_THAN_VARIANT)
            add(O_WITH_MINUS_VARIANT)
            add(O_WITH_PLUS_VARIANT)
            add(O_WITH_SLASH_VARIANT)
            add(O_WITH_TIMES_VARIANT)
            add(O_WITH_VEE_VARIANT)
            add(O_WITH_WEDGE_VARIANT)
            add(TIMES_VARIANT)
            add(BIG_SQUARE_CAP)

            // wasysym
            // "The Windings of LaTeX" -Sten 2021
            add(FEMALE)
            add(MALE)
            add(CURRENCY)
            add(PHONE)
            add(RECORDER)
            add(CLOCK)
            add(LIGHTNING_WASYSYM)
            add(POINTER)
            add(RIGHT_TRIANGLE)
            add(LEFT_TRIANGLE)
            add(UP_TRIANGLE)
            add(DOWN_TRIANGLE)
            add(AC)
            add(HF)
            add(VHF)
            add(EMPTY_BOX)
            add(PENTAGON)
            add(HEXAGON_LYING)
            add(HEXAGON_STANDING)
            add(OCTAGON)
            add(SIX_LEGGED_STAR_LYING)
            add(SIX_LEGGED_STAR_STANDING)
            add(STAR_OF_DAVID)
            add(DIAMETER)
            add(DIAMETER_MIRRORED)
            add(MEASURED_ANGLE_WASYSYM)
            add(LOZENGE_WASYSYM)
            add(MALTESE_CROSS)
            add(SMILEY)
            add(FROWNIE)
            add(BLACK_SMILEY)
            add(SUN)
            add(CHECKED)
            add(BELL)
            add(TEXT_EIGTH_NOTE)
            add(TEXT_QUARTER_NOTE)
            add(TEXT_HALF_NOTE)
            add(FULLNOTE)
            add(TWONOTES)
            add(BROKENVERT)
            add(ATARIBOX)
            add(WASYTHEREFORE)
            add(WHITE_CIRCLE)
            add(BLACK_CIRCLE)
            add(WHITE_LEFT_CIRCLE)
            add(BLACK_LEFT_CIRCLE)
            add(WHITE_RIGHT_CIRCLE)
            add(WHITE_LEFT_CIRCLE)
            add(BLACK_LEFT_CIRCLE_2)
            add(BLACK_RIGHT_CIRCLE_2)
            add(VERNAL)
            add(ASCENDING_NODE)
            add(DESCENDING_NODE)
            add(FULL_MOON)
            add(NEW_MOON)
            add(LEFT_MOON)
            add(RIGHT_MOON)
            add(ASTROSUN)
            add(MERCURY)
            add(VENUS)
            add(EARTH)
            add(MARS)
            add(JUPITER)
            add(SATURN)
            add(URANUS)
            add(NEPTUNE)
            add(PLUTO)
            add(CAPRICORNUS)
            add(AQUARIUS)
            add(PISCES)
            add(ARIES)
            add(TAURUS)
            add(GEMINI)
            add(CANCER)
            add(LEO)
            add(VIRGO)
            add(LIBRA)
            add(SCORPIO)
            add(SAGITTARIUS)
            add(CONJUNCTION)
            add(OPPOSITION)
            add(APL_STAR)
            add(APL_LOG)
            add(APL_BOX)
            add(APL_UP)
            add(APL_DOWN)
            add(APL_INPUT)
            add(APL_COMMENT)
            add(APL_INV)
            add(APL_UP_ARROW_BOX)
            add(APL_RIGHT_ARROW_BOX)
            add(APL_DOWN_ARROW_BOX)
            add(APL_LEFT_ARROW_BOX)
            add(APL_MINUS)
            add(APL_NOT, latex = "\\APLnot{}", image = "\\APLnot{}")
            add(APL_CIRCLE, latex = "\\APLcirc{}", image = "\\APLcirc{}")
            add(APL_VERTICAL_LINE, latex = "\\APLvert{}", image = "\\APLvert{}")
            add(NOT_BACKSLASH)
            add(NOT_SLASH)
            add(TEXT_BOWTIE)
            add(TEXT_ANTI_CLOCKWISE_ROUND_ARROW)
            add(TEXT_CLOCKWISE_ROUND_ARROW)
            add(PHOTON)
            add(GLUON)
            add(CENT)
            add(PERMIL)
            add(TEXT_UPSIDE_DOWN_OMEGA)
            add(THORN)
            add(CAPITAL_THORN)
            add(OPENO)
            add(INVE)
        }
    }

    /**
     * The list of all registered categories.
     * Also contains the ALL category.
     */
    val categoryList: List<SymbolCategory> = listOf(SymbolCategory.ALL) + categories.map { (category, _) -> category }

    /**
     * Flat map of all registered symbols, in order.
     */
    val symbolList: List<SymbolUiEntry> = categories.flatMap { it.value }

    /**
     * Get the operators that are in the given category.
     * The category [SymbolCategory.ALL] returns all available symbols.
     */
    operator fun get(category: SymbolCategory): List<SymbolUiEntry> = if (category == SymbolCategory.ALL) {
        symbolList
    }
    else categories[category] ?: emptyList()

    /**
     * Adds a symbol entry for a Left/Right pair that inserts \leftX ... \rightX.
     *
     * @param requireLeftRight
     *          True if the commands should be preceded with \left and \right. False if the commands alone suffice.
     */
    private fun MutableList<SymbolUiEntry>.addLeftRight(
        left: LatexCommand, right: LatexCommand, description: String, fileName: String, requireLeftRight: Boolean = true
    ) {
        val leftCmd = if (requireLeftRight) "\\left" else ""
        val rightCmd = if (requireLeftRight) "\\right" else ""
        add(
            DryUiEntry(
                description = description,
                generatedLatex = "$leftCmd${left.commandWithSlash} <caret> $rightCmd${right.commandWithSlash}",
                fileName = fileName,
                imageLatex = "$leftCmd${left.commandWithSlash}...$rightCmd${right.commandWithSlash}",
                isMathSymbol = true,
                dependency = left.dependency
            )
        )
    }

    /**
     * Adds a UI entry for the given command to the entry list.
     * For the parameters see [CommandUiEntry].
     */
    private fun MutableList<SymbolUiEntry>.add(
        command: LatexCommand,
        latex: String? = null,
        fileName: String? = null,
        description: String? = null,
        image: String? = null
    ) = add(command.toEntry(latex, fileName, description, image))

    /**
     * Turns the command into a ui entry.
     */
    private fun LatexCommand.toEntry(
        latex: String? = null,
        fileName: String? = null,
        description: String? = null,
        image: String? = null
    ) = CommandUiEntry(this, latex, fileName, description, image)

    /**
     * Adds a new category to the map and initializes the symbols.
     */
    private fun MutableMap<SymbolCategory, List<SymbolUiEntry>>.createCategory(
        name: String,
        description: String = name,
        symbolInitializer: MutableList<SymbolUiEntry>.() -> Unit
    ) {
        val category = SymbolCategory(name, description)
        this[category] = ArrayList<SymbolUiEntry>().apply {
            symbolInitializer()
        }
    }
}

package nl.hannahsten.texifyidea.ui.symbols

import nl.hannahsten.texifyidea.lang.Described

/**
 * @author Hannah Schellekens
 */
data class SymbolCategory(val name: String, override val description: String) : Described {

    override fun toString() = name

    companion object {

        val ALL = SymbolCategory("All", "Contains all available symbols.")
    }
}

package nl.hannahsten.texifyidea.ui.symbols

import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.IconLoader
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.SearchTextField
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.content.ContentFactory
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.WrapLayout
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.psiFile
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.FlowLayout
import java.util.*
import javax.swing.JButton
import javax.swing.JPanel
import javax.swing.event.DocumentEvent

/**
 * The Symbol tool window shows an overview of several symbols that can be inserted in the active latex document.
 *
 * @author Hannah Schellekens
 */
open class SymbolToolWindowFactory : ToolWindowFactory, DumbAware {

    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        val toolWindowPanel = SymbolToolWindow(project)
        val content = ContentFactory.getInstance().createContent(toolWindowPanel, "", false)
        toolWindow.contentManager.addContent(content)
    }

    // Non-idea has no concept of modules so we need to use some other criterion based on the project
    override fun isApplicable(project: Project) = project.isLatexProject()

    /**
     * The swing contents of the symbol tool window.
     */
    class SymbolToolWindow(val project: Project) : JPanel(BorderLayout(0, 0)) {

        /**
         * Control to search through the symbols that are in the currently selected category.
         */
        private val txtSearch = SearchTextField(true, "History").apply {
            addDocumentListener(object : DocumentAdapter() {
                override fun textChanged(e: DocumentEvent) {
                    updateFilters()
                }
            })
        }

        /**
         * Selects the current category if icons.
         */
        private val cboxCategory = ComboBox(SymbolCategories.categoryList.toTypedArray()).apply {
            maximumRowCount = 16
            addActionListener {
                updateFilters()
            }
        }

        /**
         * Maps a symbol category to a map mapping a symbol entry to the ui button.
         */
        private val btnSymbols = HashMap<SymbolCategory, MutableMap<SymbolUiEntry, JButton>>()

        /**
         * Panel that contains all the symbols in the current category.
         */
        private val panelSymbols = JPanel(WrapLayout(FlowLayout.LEFT, 2, 2)).apply {
            SymbolCategories.categories.forEach { (category, symbols) ->
                symbols.forEach { symbol ->
                    add(symbol.createButton(category))
                }
            }
        }

        init {
            border = JBUI.Borders.empty(8)

            add(filterPanel(), BorderLayout.NORTH)
            add(
                JBScrollPane(
                    panelSymbols,
                    JBScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                    JBScrollPane.HORIZONTAL_SCROLLBAR_NEVER
                ).apply {
                    border = JBUI.Borders.empty()
                },
                BorderLayout.CENTER
            )
        }

        /**
         * Creates a panel containing the filter controls.
         */
        private fun filterPanel() = JPanel(BorderLayout(8, 8)).apply {
            add(txtSearch, BorderLayout.CENTER)
            add(cboxCategory, BorderLayout.EAST)
        }

        /**
         * Creates the symbol button for this symbol entry.
         */
        private fun SymbolUiEntry.createButton(category: SymbolCategory) = JButton().apply {
            // Make sure the button is exactly 48x48
            minimumSize = Dimension(48, 48)
            preferredSize = Dimension(48, 48)
            maximumSize = Dimension(48, 48)

            // Load the icon with IJ's loader, dark/light mode is managed automatically this way.
            icon = IconLoader.getIcon(imagePath, SymbolToolWindowFactory::class.java)

            // Create a nice tooltip.
            val packageInfo = if (dependency == LatexPackage.DEFAULT) "" else """Package: ${dependency.name}
"""
            toolTipText = """

Command: ${generatedLatex.replace("<cursor>", "")}

                $packageInfo

$description

            """

            // Insert symbol when pressed.
            addActionListener { insertSymbol(this@createButton) }

            // Cache the button so it can be filtered later.
            val symbolMap = btnSymbols.getOrDefault(category, HashMap())
            symbolMap[this@createButton] = this
            btnSymbols[category] = symbolMap
        }

        /**
         * Shows or hides the buttons based on the filters.
         */
        private fun updateFilters() {
            val selectedCategory = cboxCategory.item
            val query = txtSearch.text.trim().replace(" ", "")

            btnSymbols.forEach { (category, symbolMap) ->
                symbolMap.forEach { (symbol, button) ->
                    button.isVisible =
                        // Selected category must match.
                        (selectedCategory == category || selectedCategory == SymbolCategory.ALL) &&
                        // When a query is typed, must match as well.
                        (query.isBlank() || symbol.queryString(category).contains(query))
                }
            }
        }

        /**
         * Inserts the symbol into the currently active document.
         */
        private fun insertSymbol(symbol: SymbolUiEntry) {
            val editor = project.currentTextEditor()?.editor ?: return
            val originalCaret = editor.caretOffset()
            val selection = editor.selectionModel

            val latex = symbol.generatedLatex
            val caretLocationInGeneratedLatex = latex.indexOf("<caret>")

            // When there is is a selection and a <caret> position is defined in the generated latex,
            // then the generated latex can be seen as 2 parts. If there is text selected, enclose the
            // selected text by these two parts. When there is no caret, it is interpreted as ending at the end
            // of the generated latex which means just appending
            if (selection.selectionEnd - selection.selectionStart > 0 && caretLocationInGeneratedLatex > 0) {
                WriteCommandAction.runWriteCommandAction(project) {
                    val parts = latex.split("<caret>")
                    editor.document.insertString(selection.selectionEnd, parts[1])
                    editor.document.insertString(selection.selectionStart, parts[0])
                    editor.caretModel.moveToOffset(selection.selectionEnd + latex.length - 7 /* <caret> */)
                }
            }
            // Nothing needs to be enclosed: just append.
            else editor.insertAtCaretAndMove(latex.replace("<caret>", ""))

            // Import the required package.
            WriteCommandAction.runWriteCommandAction(project) {
                editor.document.psiFile(project)?.insertUsepackage(symbol.dependency)
            }

            // When <caret> is defined, the cursor ends up at the location of <caret>
            if (caretLocationInGeneratedLatex < 0) return
            val newCaret = originalCaret + caretLocationInGeneratedLatex
            editor.caretModel.moveToOffset(newCaret)
        }

        /**
         * Generate a lowercase string through which to search.
         *
         * @param category
         *          The category the symbol is in.
         */
        private fun SymbolUiEntry.queryString(category: SymbolCategory? = null) = buildString {
            command?.let { append(it.commandWithSlash) }
            append(generatedLatex)
            append(dependency.name)
            append(description.replace(" ", ""))
            category?.let { append(it.name.replace(" ", "").lowercase(Locale.getDefault())) }
        }.lowercase(Locale.getDefault())
    }
}

package nl.hannahsten.texifyidea.ui.symbols

import nl.hannahsten.texifyidea.lang.Described
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexCommand

/**
 * Represents an entry in the Symbol overview tool window.
 *
 * Images representing the symbols must be saved in a `.png` format. Variants for dark themes must have a `_dark.png`
 * extension.
 *
 * @author Hannah Schellekens
 */
interface SymbolUiEntry : Described {

    /**
     * The underlying command of the symbol, if applicable.
     */
    val command: LatexCommand?

    /**
     * The latex that must be generated when the symbol gets inserted.
     * `<caret>` mars where the caret will end up after insertion.
     */
    val generatedLatex: String

    /**
     * The file name of the light theme image, ends with .png.
     */
    val fileName: String

    /**
     * The file name of the dark theme image, ends with .png.
     */
    val fileNameDark: String
        get() = fileName.replace(".png", "_dark.png")

    /**
     * The resource location of the symbol `png` image for light UIs. Image must be at most 44x44.
     */
    val imagePath: String

    /**
     * The resource location of the symbol `png` image for dark UIs.
     */
    val imagePathDark: String
        get() = imagePath.replace(".png", "_dark.png")

    /**
     * The latex used to generate the image.
     */
    val imageLatex: String

    /**
     * The package on which inserting the symbol is dependent.
     */
    val dependency: LatexPackage
        get() = command?.dependency ?: LatexPackage.DEFAULT

    /**
     * Whether this is a math symbol.
     * `false` when no command is linked to the ui entry.
     */
    val isMathSymbol: Boolean
        get() = command?.isMathMode ?: false
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.application.runReadAction
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.isDefinition

/**
 * Finds all the files in the project that are somehow related using includes.
 *
 * When A includes B and B includes C then A, B & C will all return a set containing A, B & C.
 *
 * Be careful when using this function directly over something like [ReferencedFileSetService] where the result
 * values are cached.
 *
 * @receiver The file to find the reference set of.
 * @return All the LaTeX and BibTeX files that are cross referenced between each other.
 */
// Internal because only ReferencedFileSetCache should call this
internal fun PsiFile.findReferencedFileSetWithoutCache(): Set<PsiFile> {
    // Setup.
    val project = this.project
    val includes = LatexIncludesIndex.getItems(project)

    // Find all root files.
    val roots = includes.asSequence()
        .map { it.containingFile }
        .distinct()
        .filter { it.isRoot() }
        .toSet()

    // Map root to all directly referenced files.
    val sets = HashMap<PsiFile, Set<PsiFile>>()
    for (root in roots) {
        val referenced = runReadAction { root.referencedFiles(root.virtualFile) } + root

        if (referenced.contains(this)) {
            return referenced + this
        }

        sets[root] = referenced
    }

    // Look for matching root.
    for (referenced in sets.values) {
        if (referenced.contains(this)) {
            return referenced + this
        }
    }

    return setOf(this)
}

/**
 * Finds all the files in the project that are somehow related using includes.
 *
 * When A includes B and B includes C then A, B & C will all return a set containing A, B & C.
 *
 * @return All the files that are cross referenced between each other.
 */
fun PsiFile.referencedFileSet(): Set<PsiFile> {
    return ReferencedFileSetService.getInstance().referencedFileSetOf(this)
}

/**
 * @see [BibtexEntryIndex.getIndexedEntriesInFileSet]
 */
fun PsiFile.bibtexIdsInFileSet() = BibtexEntryIndex().getIndexedEntriesInFileSet(this)

/**
 * @see [LatexCommandsIndex.getItemsInFileSet]
 */
fun PsiFile.commandsInFileSet(): Collection<LatexCommands> = LatexCommandsIndex.getItemsInFileSet(this)

/**
 * @see [LatexCommandsIndex.getItemsAndFilesInFileSet]
 */
fun PsiFile.commandsAndFilesInFileSet(): List<Pair<PsiFile, Collection<LatexCommands>>> = LatexCommandsIndex.getItemsAndFilesInFileSet(this)

/**
 * Get all the definitions in the file set.
 */
fun PsiFile.definitionsInFileSet(): Collection<LatexCommands> {
    return LatexDefinitionIndex.getItemsInFileSet(this)
        .filter { it.isDefinition() }
}

/**
 * Get all the definitions and redefinitions in the file set.
 */
fun PsiFile.definitionsAndRedefinitionsInFileSet(): Collection<LatexCommands> {
    return LatexDefinitionIndex.getItemsInFileSet(this)
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.util.appendExtension
import nl.hannahsten.texifyidea.util.magic.FileMagic
import java.awt.datatransfer.DataFlavor
import java.awt.datatransfer.Transferable
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.util.regex.Pattern
import kotlin.io.path.pathString

/**
 * @author Hannah Schellekens
 */
object FileUtil {

    /**
     * Matches the extension of a file name, including the dot.
     */
    val FILE_EXTENSION = Pattern.compile("\\.[^.]+$")!!

    /**
     * Matches the file body, including the dot
     */
    val FILE_BODY = Pattern.compile(".*\\.")!!

    /**
     * Get the FileType instance that corresponds to the given file extension.
     *
     * @param extensionWithoutDot
     *         The file extension to get the corresponding FileType instance of without a dot in
     *         front.
     * @return The corresponding FileType instance.
     */
    @JvmStatic
    fun fileTypeByExtension(extensionWithoutDot: String): FileType {
        return FileMagic.fileTypes.firstOrNull {
            it.defaultExtension == extensionWithoutDot
        } ?: LatexFileType
    }

    /**
     * Retrieves the file path relative to the root path, or `null` if the file is not a child
     * of the root.
     *
     * @param rootPath
     *         The path of the root
     * @param filePath
     *         The path of the file
     * @return The relative path of the file to the root, or `null` if the file is no child of
     * the root.
     */
    @JvmStatic
    fun pathRelativeTo(rootPath: String, filePath: String): String? {
        if (!filePath.startsWith(rootPath)) {
            return null
        }
        return filePath.substring(rootPath.length)
    }
}

/**
 * Removes the extension from a given file name.
 */
fun String.removeFileExtension() = FileUtil.FILE_EXTENSION.matcher(this).replaceAll("")!!

/**
 * Returns the extension of given filename
 */
fun String.getFileExtension(): String =
    if (this.contains(".")) FileUtil.FILE_BODY.matcher(this).replaceAll("")!! else ""

/**
 * Creates a project directory at `path` which will be marked as excluded.
 *
 * @param path The path to create the directory to.
 */
fun Module.createExcludedDir(path: String) {
    ModuleRootManager.getInstance(this).modifiableModel.addContentEntry(path)
        .addExcludeFolder(path)
}

/**
 * Retrieves the [PsiFile] for the document within the given [project].
 *
 * @param project
 *          The project scope to retrieve the psi file for.
 * @return The PSI file matching the document, or `null` when the PSI file could not be found.
 */
fun Document.psiFile(project: Project): PsiFile? = PsiDocumentManager.getInstance(project).getPsiFile(this)

/**
 * Creates a new file with a given name and given content.
 *
 * Also checks if the file already exists, and modifies the name accordingly.
 *
 * @return The created file.
 */
fun createFile(fileName: String, contents: String): File {
    val currentFileName = getUniqueFileName(fileName)

    return File(currentFileName).apply {
        createNewFile()
        LocalFileSystem.getInstance().refresh(true)
        writeText(contents, StandardCharsets.UTF_8)
    }
}

/**
 * This writes to a file without using java.io.File
 *
 * Needs to be wrapped in a writable environment
 *
 * @param project The project this is for
 * @param filePath The absolute path of the file to create. File extension provided later
 * @param text The text to add to the file
 * @param root The root path of the project? Could possibly be replaced
 * @param extension The desired file extension **without** a dot, tex by default
 *
 * @return Returns the name of the file that was created without folder names, and with the extension
 */
fun writeToFileUndoable(project: Project, filePath: String, text: String, root: String, extension: String = "tex"): String {
    val filenameNoExtension = Path.of(filePath).fileName.toString()
    val filepathNoFilename = Path.of(filePath).parent.pathString

    // for windows
    val filepathNoFilenameForwardSeperators = filepathNoFilename.replace("\\", "/")

    // Create file...but not on fs yet
    val fileFactory = PsiFileFactory.getInstance(project)
    val newfile = fileFactory.createFileFromText(
        getUniqueFileName(
            filenameNoExtension.appendExtension(extension),
            filepathNoFilename
        ),
        LatexFileType,
        text
    )

    val projectRootManager = ProjectRootManager.getInstance(project)
    val allRoots = projectRootManager.contentRoots + projectRootManager.contentSourceRoots

    // The following is going to resolve the PsiDirectory that we need to add the new file to.
    var relativePath = ""
    var bestRoot: VirtualFile? = null
    for (testFile in allRoots) {
        val rootPath = testFile.path
        if (filepathNoFilenameForwardSeperators.startsWith(rootPath)) {
            relativePath = filepathNoFilenameForwardSeperators.substring(rootPath.length)
            bestRoot = testFile
            break
        }
    }
    if (bestRoot == null) {
        throw IOException("Can't find '$root' among roots")
    }

    val dirs = relativePath.split("/".toRegex()).dropLastWhile { it.isEmpty() }
        .toTypedArray()

    var resultDir: VirtualFile = bestRoot
    if (dirs.isNotEmpty()) {
        var i = 0
        if (dirs[0].isEmpty()) i = 1

        while (i < dirs.size) {
            var subdir = resultDir.findChild(dirs[i])
            if (subdir != null) {
                if (!subdir.isDirectory) {
                    throw IOException("Expected resultDir, but got non-resultDir: " + subdir.path)
                }
            }
            else {
                subdir = resultDir.createChildDirectory(LocalFileSystem.getInstance(), dirs[i])
            }
            resultDir = subdir
            i += 1
        }
    }

    // Actually create the file on fs
    val thing = PsiManager.getInstance(project).findDirectory(resultDir)?.add(newfile)

    // This is not a common case, use as a fallback
    if (thing !is PsiFile) {
        return filenameNoExtension.appendExtension(extension)
    }

    // back to your regularly scheduled programming. Does not cast to LatexFile because virtualFile inherits from PsiFile
    return thing.virtualFile.path
        .replace(File.separator, "/")
        .replace("$root/", "")
}

/**
 * Returns the name first non-conflicting filename with the provided base name
 */
fun getUniqueFileName(fileName: String, directory: String? = null): String {
    var count = 0
    var currentFileName = fileName
    while (
        File(
            (
                if (directory != null) {
                    directory + File.separator
                }
                else ""
                ) + currentFileName
        ).exists()
    ) {
        val extension = "." + FileUtilRt.getExtension(currentFileName)
        var stripped = currentFileName.substring(0, currentFileName.length - extension.length)

        val countString = count.toString()
        if (stripped.endsWith(countString)) {
            stripped = stripped.substring(0, stripped.length - countString.length)
        }

        currentFileName = stripped + (++count) + extension
    }
    return currentFileName
}

/**
 * Get a(n external) file by its absolute path.
 */
fun getExternalFile(path: String): VirtualFile? =
    LocalFileSystem.getInstance().findFileByPath(path)

/**
 * Converts the absolute path to a relative path.
 */
fun String.toRelativePath(basePath: String): String {
    return File(basePath).toURI().relativize(File(this).toURI()).path
}

/**
 * Extracts the list of files from the Drag and Drop Transferable, only if java file list is a supported flavour.
 *
 * @return The file list transfer data, or `null` when file lists are not supported.
 */
fun Transferable.extractFiles(): List<File>? {
    if (isDataFlavorSupported(DataFlavor.javaFileListFlavor).not()) return null

    @Suppress("UNCHECKED_CAST")
    return getTransferData(DataFlavor.javaFileListFlavor) as? List<File>
}

/**
 * Extracts the first file from the Drag and Drop Transferable, only if java file list is a supported flavour.
 *
 * @return The first file in the transfer data, or `null` when file lists are not supported.
 */
fun Transferable.extractFile() = extractFiles()?.firstOrNull()

/**
 * Looks up the relative path of the file represented by the given absolute path.
 * This relative path is relative to the content roots of this ProjectRootManager.
 *
 * @return The relative path, relative to all source roots. `null` when no relative path could be found.
 */
fun ProjectRootManager.relativizePath(absoluteFilePath: String): String? = contentSourceRoots.asSequence()
    .map { it to absoluteFilePath.toRelativePath(it.path) }
    .firstOrNull { (contentRoot, relativePath) ->
        // Make sure to convert to the right file when multiple files exist in the content
        // roots with the same name. Also convert to [File]s to normalize path names.
        val original = File(absoluteFilePath)
        val candidate = File("${contentRoot.path}/$relativePath")
        candidate.absolutePath == original.absolutePath
    }
    ?.second

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.RequiredFileArgument
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.appendExtension
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * This method will try to find a file when the 'import' package is used, which means that including files have to be searched for import paths.
 *
 * Note that this method cannot use other methods that rely on the fileset, because this method is used in building the fileset.
 */
fun searchFileByImportPaths(command: LatexCommands): PsiFile? {
    // Check if import commands are used (do this now, to only search for import paths when needed)
    val allRelativeImportCommands = LatexIncludesIndex.getCommandsByNames(
        CommandMagic.relativeImportCommands,
        command.project,
        GlobalSearchScope.projectScope(command.project)
    )
    val allAbsoluteImportCommands = LatexIncludesIndex.getCommandsByNames(
        CommandMagic.absoluteImportCommands,
        command.project,
        GlobalSearchScope.projectScope(command.project)
    )
    if (allAbsoluteImportCommands.isEmpty() && allRelativeImportCommands.isEmpty()) {
        return null
    }

    // Use references to get filenames, take care not to resolve the references because this method is called during resolving them so that would be a loop. This line will take a very long time for large projects, as it has to do a lot of recursive navigation in the psi tree in order to get the text required for building the reference keys.
    val references = command.references.filterIsInstance<InputFileReference>()

    getParentDirectoryByImportPaths(command).forEach { parentDir ->
        for (reference in references) {
            val fileName = reference.key
            for (extension in reference.extensions) {
                parentDir.findFileByRelativePath(fileName.appendExtension(extension))?.let {
                    return it.psiFile(command.project)
                }
            }
        }
    }

    return null
}

/**
 * When the 'import' package is used, get all possible parent directories where a file included by the current command could hide.
 */
fun getParentDirectoryByImportPaths(command: LatexCommands): List<VirtualFile> {
    checkForAbsolutePath(command)?.let { return listOf(it) }

    val relativeSearchPaths = mutableListOf<String>()
    if (command.name in CommandMagic.relativeImportCommands) {
        relativeSearchPaths.add(command.getRequiredParameters().firstOrNull() ?: "")
    }
    else {
        relativeSearchPaths.add("")
    }

    return findRelativeSearchPathsForImportCommands(command, relativeSearchPaths)
}

/**
 * If using an absolute path to include a file.
 */
fun checkForAbsolutePath(command: LatexCommands): VirtualFile? {
    if (command.name in CommandMagic.absoluteImportCommands) {
        val absolutePath = command.getRequiredParameters().firstOrNull()
        if (absolutePath != null) {
            // No need to search further, because using an absolute path overrides the rest
            LocalFileSystem.getInstance().findFileByPath(absolutePath)?.let { return it }
        }
    }

    return null
}

fun findRelativeSearchPathsForImportCommands(command: LatexCommands, givenRelativeSearchPaths: List<String> = listOf("")): List<VirtualFile> {
    var relativeSearchPaths = givenRelativeSearchPaths.toMutableList()
    val allIncludeCommands = LatexIncludesIndex.getItems(command.project)
    // Commands which may include the current file (this is an overestimation, better would be to check for RequiredFileArguments)
    var includingCommands = allIncludeCommands.filter {
            includeCommand ->
        includeCommand.getRequiredParameters().any { it.contains(command.containingFile.name.removeFileExtension()) }
    }.filter { includeCommand ->
        // Only consider commands that can include LaTeX files
        LatexCommand.lookup(includeCommand.name)?.firstOrNull()?.getArgumentsOf(RequiredFileArgument::class.java)?.any { it.supportedExtensions.contains("tex") } == true
    }

    // Assume the containingFile might be a root file - if there are no includingCommands we will not search further anyway
    val defaultParentDir = command.containingFile.containingDirectory?.virtualFile?.findFileByRelativePath(command.requiredParameter(0) ?: "")

    // Avoid endless loop (in case of a file inclusion loop)
    val maxDepth = allIncludeCommands.size
    var counter = 0

    // Final paths
    val absoluteSearchDirs = mutableListOf<VirtualFile>()

    if (defaultParentDir != null) {
        absoluteSearchDirs.add(defaultParentDir)
    }

    // I think it's a kind of reversed BFS
    while (includingCommands.isNotEmpty() && counter < maxDepth) {
        val newSearchPaths = mutableListOf<String>()
        val handledFiles = mutableListOf<PsiFile>()
        val newIncludingcommands = mutableListOf<LatexCommands>()

        for (includingCommand in includingCommands) {
            // Stop searching when absolute path is found
            checkForAbsolutePath(includingCommand)?.let { baseDir ->
                return listOf(baseDir) + relativeSearchPaths.mapNotNull {
                    baseDir.findFileByRelativePath(it)
                }
            }

            // Each of the search paths gets prepended by one of the new relative paths found
            for (oldPath in relativeSearchPaths) {
                if (includingCommand.name in CommandMagic.relativeImportCommands) {
                    newSearchPaths.add(includingCommand.getRequiredParameters().firstOrNull() + oldPath)
                }
            }

            // Find files/commands to search next
            val file = includingCommand.containingFile
            if (file !in handledFiles) {
                val commandsIncludingThisFile = allIncludeCommands.filter { includeCommand -> includeCommand.getRequiredParameters().any { it.contains(file.name) } }
                if (commandsIncludingThisFile.isEmpty()) {
                    // Cool, we supposedly have a root file, now try to find the directory containing the file being included by command
                    for (relativePath in newSearchPaths) {
                        val searchDir = file.containingDirectory?.virtualFile?.findFileByRelativePath(relativePath) ?: continue
                        absoluteSearchDirs.add(searchDir)
                    }
                }
                newIncludingcommands.addAll(commandsIncludingThisFile)
                handledFiles.add(file)
            }
        }

        includingCommands = newIncludingcommands
        relativeSearchPaths = newSearchPaths
        counter++
    }

    return absoluteSearchDirs
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import nl.hannahsten.texifyidea.settings.sdk.TectonicSdk
import nl.hannahsten.texifyidea.util.runCommand
import java.io.File

/**
 * Cache locations of LaTeX packages in memory, because especially on Windows they can be expensive to retrieve
 * (requires a run of kpsewhich), which takes too long to do on every character typed by the user.
 * Can also be used for tex/bib files and whatever can be used with kpsewhich (probably any file that is in its search path).
 */
object LatexPackageLocationCache {

    /** Map filename with extension to full path. */
    private var cache: MutableMap<String, String?>? = null

    /**
     * Fill cache with all paths of all files in the LaTeX installation.
     * Note: this can take a long time.
     */
    fun fillCacheWithKpsewhich(project: Project) {
        // We will get all search paths that kpsewhich has, expand them and find all files
        // Source: https://www.tug.org/texinfohtml/kpathsea.html#Casefolding-search
        // We cannot just fill the cache on the fly, because then we will also run kpsewhich when the user is still typing a package name, so we will run it once for every letter typed and this is already too expensive.
        // We cannot rely on ls-R databases because they are not always populated, and running mktexlsr may run into permission issues.
        val executableName = LatexSdkUtil.getExecutableName("kpsewhich", project)
        val searchPaths = runCommand(executableName, "-show-path=tex")
        cache = runCommand(executableName, "-expand-path", searchPaths ?: ".:")?.split(File.pathSeparator)
            ?.flatMap { LocalFileSystem.getInstance().findFileByPath(it)?.children?.toList() ?: emptyList() }
            ?.filter { !it.isDirectory }
            ?.toSet()
            ?.associate { it.name to it.path }
            ?.toMutableMap() ?: mutableMapOf()
    }

    /**
     * Get the full path to the location of the package with the given name, or null in case there was any problem.
     * Note that if the package is not yet in the cache and multiple callers try to get it concurrently, then
     * the kpsewhich method will still be executed as much as there are callers.
     * If needed, this can be avoided using coroutines with a mutex (see [ReferencedFileSetCache]).
     *
     * @param name Package name with extension.
     */
    fun getPackageLocation(name: String, project: Project): String? {
        if (cache == null) {
            fillCacheWithKpsewhich(project)
        }

        // Tectonic does not have kpsewhich, but works a little differently
        val projectSdk = LatexSdkUtil.getLatexProjectSdk(project)
        val path = if (projectSdk?.sdkType is TectonicSdk) {
            (projectSdk.sdkType as TectonicSdk).getPackageLocation(name, projectSdk.homePath)
        }
        else {
            cache?.get(name)
        }
        if (path?.isBlank() == true) return null
        return path
    }
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import nl.hannahsten.texifyidea.file.BibtexFileType
import nl.hannahsten.texifyidea.file.ClassFileType
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.file.StyleFileType
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.index.LatexEnvironmentsIndex
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.magic.FileMagic
import nl.hannahsten.texifyidea.util.parser.allCommands
import nl.hannahsten.texifyidea.util.parser.getIncludedFiles
import nl.hannahsten.texifyidea.util.parser.isDefinition
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * Get the file search scope for this psi file.
 */
val PsiFile.fileSearchScope: GlobalSearchScope
    get() = GlobalSearchScope.fileScope(this)

/**
 * Looks for all file inclusions in a given file, excluding installed LaTeX packages.
 *
 * @return A list containing all included files.
 */
fun PsiFile.findInclusions(): List<PsiFile> {
    return LatexIncludesIndex.getItems(this)
        .flatMap { it.getIncludedFiles(false) }
        .toList()
}

/**
 * Checks if the file has LaTeX syntax.
 */
fun PsiFile.isLatexFile() = fileType == LatexFileType ||
    fileType == StyleFileType || fileType == ClassFileType

/**
 * Checks if the file has a `.sty` extention. This is a workaround for file type checking.
 */
fun PsiFile.isStyleFile() = virtualFile?.extension == "sty"

/**
 * Checks if the file has a `.cls` extention. This is a workaround for file type checking.
 */
fun PsiFile.isClassFile() = virtualFile?.extension == "cls"

/**
 * Looks up the argument that is in the documentclass command, and if the file is found in the project return it.
 * Note this explicitly does not find files elsewhere on the system.
 */
fun PsiFile.documentClassFileInProject() = findFile("${documentClass()}.cls")

/**
 * If the file has a \documentclass command, return the class name, null otherwise.
 */
fun PsiFile.documentClass(): String? {
    return commandsInFile().asSequence()
        .filter { it.name == "\\documentclass" }
        .firstOrNull()
        ?.requiredParameter(0)
}

/**
 * Checks if the given package is included in the file set.
 *
 * @param packageName
 *          The name of the package to check for.
 * @return `true` when there is a package with name `packageName` in the file set, `false` otherwise.
 */
fun PsiFile.isUsed(packageName: String) = this.includedPackages().map { it.name }.contains(packageName)

/**
 * Checks if the given package is included into the file set.
 *
 * @param `package`
 *          The package to check for.
 * @return `true` when there is a package `package` included in the file set, `false` otherwise.
 */
@Suppress("unused")
fun PsiFile.isUsed(`package`: LatexPackage) = isUsed(`package`.name)

/**
 * Scans the whole document (recursively) for all referenced/included files, except installed LaTeX packages.
 * Never use this directly, use the cached [referencedFileSet] instead.
 *
 * @return A collection containing all the PsiFiles that are referenced from this file.
 */
internal fun PsiFile.referencedFiles(rootFile: VirtualFile): Set<PsiFile> {
    val result = HashSet<PsiFile>()
    referencedFiles(result, rootFile)
    return result
}

/**
 * Recursive implementation of [referencedFiles].
 */
private fun PsiFile.referencedFiles(files: MutableCollection<PsiFile>, rootFile: VirtualFile) {
    LatexIncludesIndex.getItems(project, fileSearchScope).forEach command@{ command ->
        command.references.filterIsInstance<InputFileReference>()
            .mapNotNull { it.resolve(false, rootFile, true) }
            .forEach {
                // Do not re-add all referenced files if we already did that
                if (it in files) return@forEach
                files.add(it)
                it.referencedFiles(files, rootFile)
            }
    }
}

/**
 * Looks up a file relative to this file.
 *
 * @param path
 *         The path relative to this file.
 * @return The found file, or `null` when the file could not be found.
 */
fun PsiFile.findFile(path: String, extensions: Set<String>? = null): PsiFile? {
    if (project.isDisposed) return null
    val directory = containingDirectory?.virtualFile

    val file = directory?.findFile(
        path,
        extensions
            ?: FileMagic.includeExtensions
    )
        ?: return scanRoots(path, extensions)
    val psiFile = PsiManager.getInstance(project).findFile(file)
    if (psiFile == null || LatexFileType != psiFile.fileType &&
        StyleFileType != psiFile.fileType &&
        BibtexFileType != psiFile.fileType
    ) {
        return scanRoots(path, extensions)
    }

    return psiFile
}

/**
 * Looks up the file(s) included by a command relative to this file.
 *
 * @param command
 *         The include command
 * @return The found file(s), or an empty set when no files could be found.
 */
fun PsiFile.findIncludedFile(command: LatexCommands): Set<PsiFile> {
    val arguments = command.getAllRequiredArguments() ?: return emptySet()

    return arguments.filter { it.isNotEmpty() }.mapNotNull {
        val extension = FileMagic.automaticExtensions[command.name]
        if (extension != null) {
            findFile(it, setOf(extension))
        }
        else {
            findFile(it)
        }
    }.toSet()
}

/**
 * [findFile] but then it scans all content roots.
 *
 * @param path
 *         The path relative to {@code original}.
 * @return The found file, or `null` when the file could not be found.
 */
fun PsiFile.scanRoots(path: String, extensions: Set<String>? = null): PsiFile? {
    val rootManager = ProjectRootManager.getInstance(project)
    rootManager.contentSourceRoots.forEach { root ->
        val file = root.findFile(
            path,
            extensions
                ?: FileMagic.includeExtensions
        )
        if (file != null) {
            return file.psiFile(project)
        }
    }

    return null
}

/**
 * Get the corresponding document of the PsiFile.
 */
fun PsiFile.document(): Document? = PsiDocumentManager.getInstance(project).getDocument(this)

/**
 * @param commandName
 *          The name of the command including a backslash, or `null` for all commands.
 *
 * @see [LatexCommandsIndex.getItems]
 */
@JvmOverloads
fun PsiFile.commandsInFile(commandName: String? = null): Collection<LatexCommands> {
    return commandName?.let {
        this.allCommands().filter { it.name == commandName }
    } ?: this.allCommands()
}

/**
 * @see [LatexEnvironmentsIndex.getItems]
 */
fun PsiFile.environmentsInFile(): Collection<LatexEnvironment> = LatexEnvironmentsIndex.getItems(this)

/**
 * Get the editor of the file if it is currently opened. Note that the returned editor does not have to be a text editor,
 * e.g., when this file is a PDF file, the editor will be a PDF editor and not a text editor.
 *
 * @return null if the file is not opened.
 */
fun PsiFile.openedEditor(): FileEditor? {
    return FileEditorManager.getInstance(project).getSelectedEditor(virtualFile ?: return null)
}

/**
 * Get the text editor instance of the (text) file if it is currently opened.
 *
 * @return null if the file is not opened in a text editor.
 */
fun PsiFile.openedTextEditor(): Editor? = openedEditor()?.let {
    when (it) {
        is TextEditor -> it.editor
        else -> null
    }
}

/**
 * Get all the definitions in the file.
 */
fun PsiFile.definitions(): Collection<LatexCommands> {
    return LatexDefinitionIndex.getItems(this)
        .filter { it.isDefinition() }
}

/**
 * Get all the definitions and redefinitions in the file.
 */
@Suppress("unused")
fun PsiFile.definitionsAndRedefinitions(): Collection<LatexCommands> {
    return LatexDefinitionIndex.getItems(this)
}

/**
 * Get all bibtex run configurations that are probably used to compile this file.
 */
fun PsiFile.getBibtexRunConfigurations() = project
    .getLatexRunConfigurations()
    .filter { it.mainFile == findRootFile().virtualFile }
    .flatMap { it.bibRunConfigs }
    .map { it.configuration }
    .filterIsInstance<BibtexRunConfiguration>()

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.createSmartPointer
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import nl.hannahsten.texifyidea.file.listeners.VfsChangeListener
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import java.util.concurrent.ConcurrentHashMap

/**
 * Caches the values for [findReferencedFileSetWithoutCache] calls.
 *
 * @author Hannah Schellekens
 */
class ReferencedFileSetCache {

    /**
     * A cached file set value for each base file.
     *
     * The base file is the file from which the file set request came from.
     * Meaning that a base file `A` is mapped to the file set with `A` as search root, `B` is mapped to the file set
     * with `B` as search root etc.
     * It could be that multiple values are equal.
     *
     * We use VirtualFile as key instead of PsiFile, because the file set depends on virtual files,
     * but virtual files are not project-specific (can be opened in multiple projects). See [VfsChangeListener].
     * For the same reason we do not use a CachedValue, because the CachedValuesManager is project-specific.
     *
     * We use SmartPsiElementPointer to avoid storing files which have become invalid, e.g. after installing a plugin which doesn't require a restart.
     */
    private val fileSetCache = ConcurrentHashMap<VirtualFile, Set<SmartPsiElementPointer<PsiFile>>>()

    private val rootFilesCache = ConcurrentHashMap<VirtualFile, Set<SmartPsiElementPointer<PsiFile>>>()

    /**
     * The number of includes in the include index at the time the cache was last filled.
     * This is used to check if any includes were added or deleted since the last cache fill, and thus if the cache
     * needs to be refreshed.
     *
     * Note that this class is global, so multiple projects can be open.
     */
    private var numberOfIncludes = mutableMapOf<Project, Int>()

    companion object {

        private val mutex = Mutex()
    }

    /**
     * Get the file set of base file `file`.
     * When the cache is outdated, this will first update the cache.
     */
    @Synchronized
    fun fileSetFor(file: PsiFile): Set<PsiFile> {
        return getSetFromCache(file, fileSetCache)
    }

    @Synchronized
    fun rootFilesFor(file: PsiFile): Set<PsiFile> {
        return getSetFromCache(file, rootFilesCache)
    }

    /**
     * Clears the cache for base file `file`.
     */
    fun dropCaches(file: VirtualFile) {
        fileSetCache.remove(file)
        rootFilesCache.remove(file)
    }

    fun dropAllCaches() {
        fileSetCache.keys.forEach { fileSetCache.remove(it) }
        rootFilesCache.keys.forEach { rootFilesCache.remove(it) }
    }

    /**
     * Since we have to calculate the fileset to fill the root file or fileset cache, we make sure to only do that
     * once and then fill both caches with all the information we have.
     */
    private fun updateCachesFor(requestedFile: PsiFile) {
        val fileset = requestedFile.findReferencedFileSetWithoutCache()
        for (file in fileset) {
            fileSetCache[file.virtualFile] = fileset.map { it.createSmartPointer() }.toSet()
        }

        val rootfiles = requestedFile.findRootFilesWithoutCache(fileset)
        for (file in fileset) {
            rootFilesCache[file.virtualFile] = rootfiles.map { it.createSmartPointer() }.toSet()
        }
    }

    /**
     * In a thread-safe way, get the value from the cache and if needed refresh the cache first.
     */
    private fun getSetFromCache(file: PsiFile, cache: ConcurrentHashMap<VirtualFile, Set<SmartPsiElementPointer<PsiFile>>>): Set<PsiFile> {
        return if (file.virtualFile != null) {
            // getOrPut cannot be used because it will still execute the defaultValue function even if the key is already in the map (see its javadoc)
            // Wrapping the code with synchronized (myLock) { ... } also didn't work
            // Hence we use a mutex to make sure the expensive findReferencedFileSet function is only executed when needed
            runBlocking {
                // Do NOT use a coroutine here, because then when typing (so the caller is e.g. gutter icons, inspections, line makers etc.) somehow the following (at least the runReadAction parts) will block the UI. Note that certain user-triggered actions (think run configuration) will still lead to this blocking the UI if not run in the background explicitly
                mutex.withLock {
                    // Use the keys of the whole project, because suppose a new include includes the current file, it could be anywhere in the project
                    // Note that LatexIncludesIndex.getItems(file.project) may be a slow operation and should not be run on EDT
                    val includes = LatexIncludesIndex.getItems(file.project)
                    val numberOfIncludesChanged = if (includes.size != numberOfIncludes[file.project]) {
                        numberOfIncludes[file.project] = includes.size
                        dropAllCaches()
                        true
                    }
                    else {
                        false
                    }

                    if (!cache.containsKey(file.virtualFile) || numberOfIncludesChanged) {
                        updateCachesFor(file)
                    }
                }
            }
            cache[file.virtualFile]?.mapNotNull { it.element }?.toSet() ?: setOf(file)
        }
        else {
            setOf(file)
        }
    }
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile

/**
 * @author Hannah Schellekens
 */
interface ReferencedFileSetService {

    companion object {

        @JvmStatic
        fun getInstance(): ReferencedFileSetService {
            return ApplicationManager.getApplication().getService(ReferencedFileSetService::class.java)
        }
    }

    /**
     * [findReferencedFileSetWithoutCache], but then with cached values.
     */
    fun referencedFileSetOf(psiFile: PsiFile): Set<PsiFile>

    fun rootFilesOf(psiFile: PsiFile): Set<PsiFile>

    /**
     * Invalidates the caches for the given file.
     */
    fun dropCaches(file: VirtualFile)

    fun dropAllCaches()
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.application.runReadAction
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.lang.DefaultEnvironment
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.SUBFILES
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.lang.magic.DefaultMagicKeys
import nl.hannahsten.texifyidea.lang.magic.magicComment
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.getEnvironmentName
import nl.hannahsten.texifyidea.util.getLatexRunConfigurations
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.parser.childrenOfType

/**
 * Uses the fileset cache to find all root files in the fileset.
 * Note that each root file induces a fileset, so a file could be in multiple filesets.
 */
fun PsiFile.findRootFilesWithoutCache(fileset: Set<PsiFile>): Set<PsiFile> {
    val magicComment = runReadAction { magicComment() }
    val roots = mutableSetOf<PsiFile>()

    if (magicComment.contains(DefaultMagicKeys.ROOT)) {
        val path = magicComment.value(DefaultMagicKeys.ROOT) ?: ""
        this.findFile(path)?.let { roots.add(it) }
    }

    if (this.isRoot()) {
        roots.add(this)
    }

    roots.addAll(fileset.filter { it.isRoot() })

    return if (roots.isEmpty()) setOf(this) else roots
}

/**
 * Gets the first file from the root files found by [findRootFilesWithoutCache] which is stored in the cache.
 *
 * As a best guess, get the first of the root files returned by [findRootFiles].
 *
 * Note: LaTeX Files can have more than one * root file, so using [findRootFiles] and explicitly handling the cases of
 * multiple root files is preferred over using [findRootFile].
 */
fun PsiFile.findRootFile(): PsiFile = findRootFiles().firstOrNull() ?: this

/**
 * Gets the set of files that are the root files of `this` file.
 */
fun PsiFile.findRootFiles(): Set<PsiFile> = ReferencedFileSetService.getInstance().rootFilesOf(this)

/**
 * Checks whether the psi file is a tex document root or not.
 *
 * A document root is where the compilation of a tex file starts.
 *
 * @return `true` if the file is a tex document root, `false` if the file is not a root.
 */
fun PsiFile.isRoot(): Boolean {
    if (fileType != LatexFileType) {
        return false
    }

    // Function to avoid unnecessary evaluation
    fun documentClass() = this.commandsInFile().find { it.name == LatexGenericRegularCommand.DOCUMENTCLASS.cmd }

    fun documentEnvironment() = this.childrenOfType(LatexEnvironment::class).any { it.getEnvironmentName() == DefaultEnvironment.DOCUMENT.environmentName }

    // If the file uses the subfiles documentclass, then it is a root file in the sense that all file inclusions
    // will be relative to this file. Note that it may include the preamble of a different file (using optional parameter of \documentclass)
    fun usesSubFiles() = documentClass()?.getRequiredParameters()?.contains(SUBFILES.name) == true

    // Go through all run configurations, to check if there is one which contains the current file.
    // If so, then we assume that the file is compilable and must be a root file.
    val isMainFileInAnyConfiguration = project.getLatexRunConfigurations().any { it.mainFile == this.virtualFile }

    return runReadAction { isMainFileInAnyConfiguration || documentEnvironment() || usesSubFiles() }
}

/**
 * Get all required arguments, also if comma separated in a group.
 * e.g. \mycommand{arg1,arg2}{arg3} will return [arg1, arg2, arg3].
 */
fun LatexCommands.getAllRequiredArguments(): List<String>? {
    val required = getRequiredParameters()
    if (required.isEmpty()) return null
    return required.flatMap { it.split(',') }
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.InvalidVirtualFileAccessException
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.util.appendExtension
import java.io.File

/**
 * Looks up the PsiFile that corresponds to the Virtual File.
 */
fun VirtualFile.psiFile(project: Project): PsiFile? {
    if (!this.isValid) return null
    return PsiManager.getInstance(project).findFile(this)
}

fun VirtualFile.findVirtualFileByAbsoluteOrRelativePath(filePath: String): VirtualFile? {
    val isAbsolute = File(filePath).isAbsolute
    return if (!isAbsolute) {
        findFileByRelativePath(filePath)
    }
    else {
        LocalFileSystem.getInstance().findFileByPath(filePath)
    }
}

/**
 * Try to find the virtual file, as absolute path or relative to a content root.
 */
fun findVirtualFileByAbsoluteOrRelativePath(path: String, project: Project): VirtualFile? {
    if (path.isBlank()) return null

    val fileSystem = LocalFileSystem.getInstance()

    val file = fileSystem.findFileByPath(path)
    if (file != null) {
        return file
    }
    else {
        // Maybe it is a relative path
        ProjectRootManager.getInstance(project).contentRoots.forEach { root ->
            root.findFileByRelativePath(path)?.let { return it }
        }
    }

    return null
}

/**
 * Looks for a certain file, relative to this directory or if the given path is absolute use that directly.
 *
 * When this is a file (instead of a directory) it doesn't find files that are in
 * the same directory as this file. When that is your goal, pass in the parent directory instead.
 *
 * First looks if the file including extensions exists, when it doesn't it tries to append all
 * possible extensions until it finds a good one.
 *
 * @param filePath
 *         The name of the file relative to the directory, or an absolute path.
 * @param extensions
 *         Set of all supported extensions to look for.
 * @return The matching file, or `null` when the file couldn't be found.
 */
fun VirtualFile.findFile(filePath: String, extensions: Set<String> = emptySet()): VirtualFile? {
    try {
        val isAbsolute = File(filePath).isAbsolute
        var file = if (!isAbsolute) {
            findFileByRelativePath(filePath)
        }
        else {
            LocalFileSystem.getInstance().findFileByPath(filePath)
        }
        if (file != null && !file.isDirectory) return file

        extensions.forEach { extension ->
            val lookFor = filePath.appendExtension(extension)
            file = if (!isAbsolute) {
                findFileByRelativePath(lookFor)
            }
            else {
                LocalFileSystem.getInstance().findFileByPath(lookFor)
            }

            if (file != null && !file!!.isDirectory) return file
        }
    }
    // #2248
    catch (ignored: InvalidVirtualFileAccessException) {}

    return null
}

/**
 * Find all child directories recursively, including [this] if it is a directory.
 */
fun VirtualFile.allChildDirectories(): Set<VirtualFile> {
    val set = mutableSetOf<VirtualFile>()
    allChildDirectories(set)
    return set
}

private fun VirtualFile.allChildDirectories(dirs: MutableSet<VirtualFile>) {
    if (isDirectory) {
        dirs.add(this)
        children.forEach {
            it.allChildDirectories(dirs)
        }
    }
}

/**
 * Recursively finds all files in a directory (thus, also the files in sub-directories etc.)
 */
fun VirtualFile.allChildFiles(): Set<VirtualFile> {
    val set = HashSet<VirtualFile>()
    allChildFiles(set)
    return set
}

/**
 * Recursive implementation of [allChildFiles].
 */
private fun VirtualFile.allChildFiles(files: MutableSet<VirtualFile>) {
    if (isDirectory) {
        children.forEach {
            it.allChildFiles(files)
        }
    }
    else files.add(this)
}

package nl.hannahsten.texifyidea.util.labels

import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * Finds all the defined bibtex labels in the fileset of the file.
 *
 * @return A set containing all labels that are defined in the fileset of the given file.
 */
fun PsiFile.findBibtexLabelsInFileSetAsSequence(): Sequence<String> = findBibtexItems().asSequence()
    .mapNotNull {
        when (it) {
            is BibtexEntry -> it.name
            is LatexCommands -> it.requiredParameter(0)
            else -> null
        }
    }

/**
 * Finds all specified bibtex entries
 */
fun PsiFile.findBibtexItems(): Collection<PsiElement> {
    val bibtex = BibtexEntryIndex().getIndexedEntriesInFileSet(this)
    val bibitem = findBibitemCommands().toList()
    return (bibtex + bibitem)
}

/**
 * Finds all \\bibitem-commands in the document
 */
fun PsiFile.findBibitemCommands(): Sequence<LatexCommands> = this.commandsInFileSet().asSequence()
    .filter { it.name == "\\bibitem" }

package nl.hannahsten.texifyidea.util.labels

import com.intellij.psi.PsiElement
import com.jetbrains.rd.util.first
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.getIdentifier
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import nl.hannahsten.texifyidea.util.parser.toStringMap

/**
 * Extracts the label element (so the element that should be resolved to) from the PsiElement given that the PsiElement represents a label.
 */
fun PsiElement.extractLabelElement(): PsiElement? {
    fun getLabelParameterText(command: LatexCommandWithParams): LatexParameterText {
        val optionalParameters = command.getOptionalParameterMap()
        val labelEntry = optionalParameters.filter { pair -> pair.key.toString() == "label" }.first()
        val contentList = labelEntry.value?.keyValContentList ?: emptyList()
        return contentList.firstOrNull { c -> c.parameterText != null }?.parameterText
            ?: contentList.first { c -> c.parameterGroup != null }.parameterGroup!!.parameterGroupText!!.parameterTextList.first()
    }

    return when (this) {
        is BibtexEntry -> firstChildOfType(BibtexId::class)
        is LatexCommands -> {
            if (CommandMagic.labelAsParameter.contains(name)) {
                return getLabelParameterText(this)
            }
            else {
                // For now just take the first label name (may be multiple for user defined commands)
                val info = CommandManager.labelAliasesInfo.getOrDefault(name, null)
                val position = info?.positions?.firstOrNull() ?: 0

                // Skip optional parameters for now
                this.parameterList.mapNotNull { it.requiredParam }.getOrNull(position)
                    ?.firstChildOfType(LatexParameterText::class)
            }
        }
        is LatexEnvironment -> {
            if (EnvironmentMagic.labelAsParameter.contains(getEnvironmentName())) {
                getLabelParameterText(beginCommand)
            }
            else {
                null
            }
        }
        else -> null
    }
}

/**
 * Extracts the label name from the PsiElement given that the PsiElement represents a label.
 * This function should be quick to execute, because it may be called for all labels in the project very often.
 *
 * @param referencingFileSetCommands All the commands in the fileset containing the element referencing [this], which can make a different for what the label name should be, as using the xr package labels from other filesets can be included with a prefix.
 */
fun PsiElement.extractLabelName(referencingFileSetCommands: Collection<LatexCommands>? = null): String {
    return when (this) {
        is BibtexEntry -> this.getIdentifier()
        is LatexCommands -> {
            if (CommandMagic.labelAsParameter.contains(name)) {
                getOptionalParameterMap().toStringMap()["label"] ?: ""
            }
            else {
                // For now just take the first label name (which may be multiple for user defined commands)
                val info = CommandManager.labelAliasesInfo.getOrDefault(name, null)
                val position = info?.positions?.firstOrNull() ?: 0
                var prefix = info?.prefix ?: ""

                // Check if there is any prefix given by the xr package
                if (referencingFileSetCommands != null) {
                    referencingFileSetCommands
                        // Don't think there can be multiple, at least I wouldn't know what prefix it would use
                        .firstOrNull { it.name == LatexGenericRegularCommand.EXTERNALDOCUMENT.commandWithSlash }
                        ?.parameterList?.firstNotNullOfOrNull { it.optionalParam }
                        ?.text?.trim('[', ']')
                        ?.let { prefix = it }
                }

                // Skip optional parameters for now (also below and in
                prefix + this.requiredParameter(position)
            }
        }

        is LatexEnvironment -> this.getLabel() ?: ""
        else -> text
    }
}

package nl.hannahsten.texifyidea.util.labels

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexParameterLabeledCommandsIndex
import nl.hannahsten.texifyidea.index.LatexParameterLabeledEnvironmentsIndex
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.files.psiFile

/**
 * Finds all the defined labels in the fileset of the file.
 *
 * @return A set containing all labels that are defined in the fileset of the given file.
 */
fun PsiFile.findLatexAndBibtexLabelStringsInFileSet(): Set<String> =
    (findLatexLabelStringsInFileSetAsSequence() + findBibtexLabelsInFileSetAsSequence()).toSet()

/**
 * Finds all the defined latex labels in the fileset of the file.
 * May contain duplicates.
 *
 * @return A set containing all labels that are defined in the fileset of the given file.
 */
fun PsiFile.findLatexLabelStringsInFileSetAsSequence(): Sequence<String> {
    val allCommands = this.commandsInFileSet()
    return findLatexLabelingElementsInFileSet().map { it.extractLabelName(referencingFileSetCommands = allCommands) }
}

/**
 * All labels in this file.
 */
fun PsiFile.findLatexLabelingElementsInFile(): Sequence<PsiElement> = sequenceOf(
    findLabelingCommandsInFile(),
    LatexParameterLabeledEnvironmentsIndex.getItems(this).asSequence(),
    LatexParameterLabeledCommandsIndex.getItems(this).asSequence()
).flatten()

/**
 * All labels in the fileset.
 * May contain duplicates.
 */
fun PsiFile.findLatexLabelingElementsInFileSet(): Sequence<PsiElement> = sequenceOf(
    findLabelingCommandsInFileSet(),
    LatexParameterLabeledEnvironmentsIndex.getItemsInFileSet(this).asSequence(),
    LatexParameterLabeledCommandsIndex.getItemsInFileSet(this).asSequence()
).flatten()

/**
 * Make a sequence of all commands in the file set that specify a label. This does not include commands which define a label via an
 * optional parameter.
 */
fun PsiFile.findLabelingCommandsInFileSet(): Sequence<LatexCommands> {
    // If using the xr package to include label definitions in external files, include those external files when searching for labeling commands in the fileset
    val externalCommands = this.findXrPackageExternalDocuments().flatMap { it.commandsInFileSet() }
    return (this.commandsInFileSet() + externalCommands).asSequence().findLatexCommandsLabels(this.project)
}

/**
 * Find external files which contain label definitions, as used by the xr package, which are called with \externaldocument anywhere in the fileset.
 */
fun PsiFile.findXrPackageExternalDocuments(): List<PsiFile> {
    return this.commandsInFileSet()
        .filter { it.name == LatexGenericRegularCommand.EXTERNALDOCUMENT.commandWithSlash }
        .flatMap { it.references.filterIsInstance<InputFileReference>() }
        .mapNotNull { it.findAnywhereInProject(it.key)?.psiFile(project) }
}

/**
 * @see [findLabelingCommandsInFileSet] but then only for commands in this file.
 */
fun PsiFile.findLabelingCommandsInFile(): Sequence<LatexCommands> {
    return this.commandsInFile().asSequence().findLatexCommandsLabels(this.project)
}

/*
 * Filtering sequence or collection
 */

/**
 * Finds all the labeling commands within the collection of PsiElements.
 *
 * @return A collection of all label commands.
 */
fun Collection<PsiElement>.findLatexCommandsLabels(project: Project): Collection<LatexCommands> {
    val commandNames = project.getLabelDefinitionCommands()
    return filterIsInstance<LatexCommands>().filter { commandNames.contains(it.name) }
}

/**
 * Finds all the labeling commands within the sequence of PsiElements.
 *
 * @return A sequence of all label commands.
 */
fun Sequence<PsiElement>.findLatexCommandsLabels(project: Project): Sequence<LatexCommands> {
    val commandNames = project.getLabelDefinitionCommands()
    return filterIsInstance<LatexCommands>().filter { commandNames.contains(it.name) }
}

package nl.hannahsten.texifyidea.util.labels

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.index.LatexParameterLabeledCommandsIndex
import nl.hannahsten.texifyidea.index.LatexParameterLabeledEnvironmentsIndex
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * Finds all defined labels within the project, including bibtex entries.
 *
 * @return The found label commands.
 */
fun Project.findAllLabelsAndBibtexIds(): Collection<PsiElement> {
    val commands = LatexCommandsIndex.getItems(this).findLatexCommandsLabels(this)
    val bibtexIds = BibtexEntryIndex().getIndexedEntries(this)
    val environments = LatexParameterLabeledEnvironmentsIndex.getItems(this)
    val parameterLabeledCommands = LatexParameterLabeledCommandsIndex.getItems(this)
    val result = ArrayList<PsiElement>(commands)
    result.addAll(bibtexIds)
    result.addAll(environments)
    result.addAll(parameterLabeledCommands)
    return result
}

/**
 * All commands that represent a reference to a label, including user defined commands.
 */
fun Project.getLabelReferenceCommands(): Set<String> {
    CommandManager.updateAliases(CommandMagic.labelReferenceWithoutCustomCommands, this)
    return CommandManager.getAliases(CommandMagic.labelReferenceWithoutCustomCommands.first())
}

/**
 * Get all commands defining labels, including user defined commands. This will not check if the aliases need to be updated.
 */
fun getLabelDefinitionCommands() = CommandManager.getAliases(CommandMagic.labelDefinitionsWithoutCustomCommands.first())

/**
 * Get all commands defining labels, including user defined commands.
 * If you need to know which parameters of user defined commands define a label, use [CommandManager.labelAliasesInfo].
 *
 * This will check if the cache of user defined commands needs to be updated, based on the given project, and therefore may take some time.
 */
fun Project.getLabelDefinitionCommands(): Set<String> {
    // Check if updates are needed
    CommandManager.updateAliases(CommandMagic.labelDefinitionsWithoutCustomCommands, this)
    return CommandManager.getAliases(CommandMagic.labelDefinitionsWithoutCustomCommands.first())
}

package nl.hannahsten.texifyidea.util.magic

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand

object ColorMagic {

    /**
     * All commands that have a color as an argument.
     */
    val takeColorCommands = LatexRegularCommand.values()
        .filter { cmd -> cmd.arguments.map { it.name }.contains("color") }
        .map { it.command }

    /**
     * All commands that define a new color.
     */
    val colorDefinitions = LatexRegularCommand.values()
        .filter { cmd -> cmd.dependency == LatexPackage.XCOLOR }
        .filter { cmd -> cmd.arguments.map { it.name }.contains("name") }

    val colorCommands = takeColorCommands + colorDefinitions.map { it.command }

    val defaultXcolors = mapOf(
        "red" to 0xff0000,
        "green" to 0x00ff00,
        "blue" to 0x0000ff,
        "cyan" to 0x00ffff,
        "magenta" to 0xff00ff,
        "yellow" to 0xffff00,
        "black" to 0x000000,
        "gray" to 0x808080,
        "white" to 0xffffff,
        "darkgray" to 0x404040,
        "lightgray" to 0xbfbfbf,
        "brown" to 0xfb8040,
        "lime" to 0xbfff00,
        "olive" to 0x808000,
        "orange" to 0xff8000,
        "pink" to 0xffbfbf,
        "purple" to 0xbf0040,
        "teal" to 0x008080,
        "violet" to 0x800080
    )
}

@file:Suppress("MemberVisibilityCanBePrivate")

package nl.hannahsten.texifyidea.util.magic

import com.intellij.openapi.project.Project
import com.intellij.ui.Gray
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.lang.commands.Argument
import nl.hannahsten.texifyidea.lang.commands.LatexBiblatexCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.LatexGenericMathCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexGlossariesCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexIfCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexListingCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexMathtoolsRegularCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexNatbibCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexNewDefinitionCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexOperatorCommand.*
import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand
import nl.hannahsten.texifyidea.lang.commands.LatexUncategorizedStmaryrdSymbols.BIG_SQUARE_CAP
import nl.hannahsten.texifyidea.lang.commands.LatexXparseCommand.*

object CommandMagic {

    /**
     * LaTeX commands that make the text take up more vertical space.
     */
    val high = hashSetOf(
        FRAC.cmd, DFRAC.cmd, SQUARE_ROOT.cmd, SUM.cmd, INTEGRAL.cmd, DOUBLE_INTEGRAL.cmd, TRIPLE_INTEGRAL.cmd,
        QUADRUPLE_INTEGRAL.cmd, N_ARY_PRODUCT.cmd, N_ARY_UNION.cmd, N_ARY_INTERSECTION.cmd,
        N_ARY_SQUARE_UNION.cmd, BIG_SQUARE_CAP.cmd
    )

    /**
     * Level of labeled commands.
     */
    val labeledLevels: Map<LatexCommand, Int> = mapOf(
        // See page 23 of the LaTeX Companion
        PART to -1, // actually, it is level 0 in classes that do not define \chapter and -1 in book and report
        CHAPTER to 0,
        SECTION to 1,
        SUBSECTION to 2,
        SUBSUBSECTION to 3,
        PARAGRAPH to 4,
        SUBPARAGRAPH to 5
    )

    /** Section commands sorted from large to small. */
    val sectioningCommands = labeledLevels.entries.sortedBy { it.value }.map { it.key }

    /**
     * Commands that define a label via an optional parameter
     */
    @JvmField
    val labelAsParameter = hashSetOf(LSTINPUTLISTING.cmd)

    /**
     * All commands that mark some kind of section.
     */
    val sectionMarkers = listOf(
        PART,
        CHAPTER,
        SECTION,
        SUBSECTION,
        SUBSUBSECTION,
        PARAGRAPH,
        SUBPARAGRAPH
    ).map { it.cmd }

    /**
     * The colours that each section separator has.
     */
    val sectionSeparatorColors = mapOf(
        PART.cmd to Gray._152,
        CHAPTER.cmd to Gray._172,
        SECTION.cmd to Gray._182,
        SUBSECTION.cmd to Gray._202,
        SUBSUBSECTION.cmd to Gray._212,
        PARAGRAPH.cmd to Gray._222,
        SUBPARAGRAPH.cmd to Gray._232
    )

    /**
     * LaTeX commands that increase a counter that can be labeled.
     */
    val increasesCounter =
        hashSetOf(CAPTION.cmd, CAPTIONOF.cmd, CHAPTER.cmd, SECTION.cmd, SUBSECTION.cmd, ITEM.cmd, LSTINPUTLISTING.cmd)

    /**
     * All commands that represent a reference to a label, excluding user defined commands.
     */
    val labelReferenceWithoutCustomCommands = LatexRegularCommand.ALL
        .filter { cmd -> cmd.arguments.any { it.type == Argument.Type.LABEL } }
        .map { it.cmd }.toSet()

    /**
     * All commands that represent a reference to a bibliography entry/item.
     */
    val bibliographyReference = hashSetOf(
        CITE,
        NOCITE,
        CITEP,
        CITEP_STAR,
        CITET,
        CITET_STAR,
        CITEP,
        CITEP_STAR_CAPITALIZED,
        CITET_CAPITALIZED,
        CITET_STAR_CAPITALIZED,
        CITEALP,
        CITEALP_STAR,
        CITEALT,
        CITEALT_STAR,
        CITEALP_CAPITALIZED,
        CITEALP_STAR_CAPITALIZED,
        CITEALT_CAPITALIZED,
        CITEALT_STAR_CAPITALIZED,
        CITEAUTHOR,
        CITEAUTHOR_STAR,
        CITEAUTHOR_CAPITALIZED,
        CITEAUTHOR_STAR_CAPITALIZED,
        CITEYEAR,
        CITEYEARPAR,
        PARENCITE,
        PARENCITE_CAPITALIZED,
        FOOTCITE,
        FOOTCITETEXT,
        TEXTCITE,
        TEXTCITE_CAPITALIZED,
        SMARTCITE,
        SMARTCITE_CAPITALIZED,
        CITE_STAR,
        PARENCITE_STAR,
        SUPERCITE,
        AUTOCITE,
        AUTOCITE_CAPITALIZED,
        AUTOCITE_STAR,
        AUTOCITE_STAR_CAPITALIZED,
        CITETITLE,
        CITETITLE_STAR,
        CITEYEAR_STAR,
        CITEDATE,
        CITEDATE_STAR,
        CITEURL,
        VOLCITE,
        VOLCITE_CAPITALIZED,
        PVOLCITE,
        PVOLCITE_CAPITALIZED,
        FVOLCITE,
        FVOLCITE_CAPITALIZED,
        FTVOLCITE,
        SVOLCITE,
        SVOLCITE_CAPITALIZED,
        TVOLCITE,
        TVOLCITE_CAPITALIZED,
        AVOLCITE,
        AVOLCITE_CAPITALIZED,
        FULLCITE,
        FOOTFULLCITE,
        NOTECITE,
        NOTECITE_CAPITALIZED,
        PNOTECITE,
        FNOTECITE
    ).map { it.cmd }.toSet()

    /**
     * All commands that define a glossary entry of the glossaries package (e.g. \newacronym).
     */
    val glossaryEntry =
        hashSetOf(NEWGLOSSARYENTRY, LONGNEWGLOSSARYENTRY, NEWACRONYM, NEWABBREVIATION).map { it.cmd }.toSet()

    /**
     * All commands that reference a glossary entry from the glossaries package (e.g. \gls).
     */
    val glossaryReference = hashSetOf(GLS, GLSUPPER, GLSPLURAL, GLSPLURALUPPER).map { it.cmd }.toSet()

    /**
     * All commands that represent some kind of reference (think \ref and \cite).
     */
    val reference = labelReferenceWithoutCustomCommands + bibliographyReference

    /**
     * Commands from the import package which require an absolute path as first parameter.
     */
    val absoluteImportCommands = setOf(INCLUDEFROM.cmd, INPUTFROM.cmd, IMPORT.cmd)

    /**
     * Commands from the import package which require a relative path as first parameter.
     */
    val relativeImportCommands = setOf(SUBIMPORT.cmd, SUBINPUTFROM.cmd, SUBINCLUDEFROM.cmd)

    /**
     * All commands that define labels and that are present by default.
     * To include user defined commands, use [getLabelDefinitionCommands] (may be significantly slower).
     */
    val labelDefinitionsWithoutCustomCommands = setOf(LABEL.cmd)

    /**
     * Get all commands defining labels, including user defined commands.
     * If you need to know which parameters of user defined commands define a label, use [CommandManager.labelAliasesInfo].
     *
     * This will check if the cache of user defined commands needs to be updated, based on the given project, and therefore may take some time.
     */
    fun getLabelDefinitionCommands(project: Project): Set<String> {
        // Check if updates are needed
        CommandManager.updateAliases(labelDefinitionsWithoutCustomCommands, project)
        return CommandManager.getAliases(labelDefinitionsWithoutCustomCommands.first())
    }

    /**
     * Get all commands defining labels, including user defined commands. This will not check if the aliases need to be updated.
     */
    fun getLabelDefinitionCommands() = CommandManager.getAliases(labelDefinitionsWithoutCustomCommands.first())

    /**
     * All commands that define bibliography items.
     */
    val bibliographyItems = setOf(BIBITEM.cmd)

    /**
     * All math operators without a leading slash.
     *
     * Reference [Unofficial LaTeX2e reference manual](https://latexref.xyz/Math-functions.html)
     */
    @JvmField
    val slashlessMathOperators = hashSetOf(
        INVERSE_COSINE, INVERSE_SINE, INVERSE_TANGENT, ARGUMENT, BMOD, COSINE, HYPERBOLIC_COSINE, COTANGENT,
        HYPERBOLIC_COTANGENT, COSECANT, DEGREES, DERMINANT, DIMENSION, EXPONENTIAL, GREATEST_COMMON_DIVISOR,
        HOMOMORPHISM, INFINUM, KERNEL, BASE_2_LOGARITHM, LIMIT, LIMIT_INFERIOR, LIMIT_SUPERIOR,
        NATURAL_LOGARITHM, LOGARITHM, MAXIMUM, MINIMUM, PMOD, PROBABILITY, SECANT, SINE,
        HYPERBOLIC_SINE, SUPREMUM, TANGENT, HBOLICTANGENT
    )

    /**
     * All commands that define regular commands, and that require that the command is not already defined.
     */
    val regularStrictCommandDefinitions = hashSetOf(
        NEWCOMMAND.cmd,
        NEWCOMMAND_STAR.cmd,
        NEWIF.cmd,
        NEWDOCUMENTCOMMAND.cmd,
    )

    /**
     * Commands that define other command but don't complain if it is already defined.
     */
    val flexibleCommandDefinitions = setOf(
        PROVIDECOMMAND, // Does nothing if command exists
        PROVIDECOMMAND_STAR,
        PROVIDEDOCUMENTCOMMAND, // Does nothing if command exists
        DECLAREDOCUMENTCOMMAND,
        DEF,
        LET,
    ).map { it.cmd }

    /**
     * All commands that define or redefine other commands, whether it exists or not.
     */
    val commandRedefinitions = setOf(
        RENEWCOMMAND,
        RENEWCOMMAND_STAR,
        CATCODE, // Not really redefining commands, but characters
    ).map { it.cmd } + flexibleCommandDefinitions

    /**
     * All commands that define or redefine regular commands.
     */
    val regularCommandDefinitionsAndRedefinitions = regularStrictCommandDefinitions + commandRedefinitions

    /**
     * All commands that define commands that should be used exclusively
     * in math mode.
     */
    val mathCommandDefinitions = hashSetOf(
        DECLARE_MATH_OPERATOR.cmd,
        DECLARE_PAIRED_DELIMITER.cmd,
        DECLARE_PAIRED_DELIMITER_X.cmd,
        DECLARE_PAIRED_DELIMITER_XPP.cmd
    )

    /**
     * All commands that can define regular commands.
     */
    val commandDefinitions = regularStrictCommandDefinitions + mathCommandDefinitions + flexibleCommandDefinitions

    /**
     * All commands that (re)define new commands.
     */
    val commandDefinitionsAndRedefinitions = regularCommandDefinitionsAndRedefinitions + mathCommandDefinitions

    /**
     * All commands that define new documentclasses.
     */
    val classDefinitions = hashSetOf(PROVIDESCLASS.cmd)

    /**
     * All commands that define new packages.
     */
    val packageDefinitions = hashSetOf(PROVIDESPACKAGE.cmd)

    /**
     * All commands that define new environments.
     */
    val environmentDefinitions = hashSetOf(
        NEWENVIRONMENT,
        NEWTHEOREM,
        NEWDOCUMENTENVIRONMENT,
        PROVIDEDOCUMENTENVIRONMENT,
        DECLAREDOCUMENTENVIRONMENT,
        NEWTCOLORBOX,
        DECLARETCOLORBOX,
        NEWTCOLORBOX_,
        PROVIDETCOLORBOX,
    ).map { it.cmd }

    /**
     * All commands that define or redefine other environments, whether it exists or not.
     */
    val environmentRedefinitions = hashSetOf(
        RENEWENVIRONMENT.cmd,
        RENEWTCOLORBOX.cmd,
        RENEWTCOLORBOX_.cmd,
    )

    /**
     * All commands that define stuff like classes, environments, and definitions.
     */
    val definitions = commandDefinitionsAndRedefinitions + classDefinitions + packageDefinitions + environmentDefinitions

    /**
     * Commands for which TeXiFy-IDEA has essential custom behaviour and which should not be redefined.
     */
    val fragile = hashSetOf(
        "\\addtocounter", "\\begin", "\\chapter", "\\def", "\\documentclass", "\\end",
        "\\include", "\\includeonly", "\\input", "\\label", "\\let", "\\newcommand",
        "\\overline", "\\paragraph", "\\part", "\\renewcommand", "\\section", "\\setcounter",
        "\\sout", "\\subparagraph", "\\subsection", "\\subsubsection", "\\textbf",
        "\\textit", "\\textsc", "\\textsl", "\\texttt", "\\underline", "\\[", "\\]",
        "\\newenvironment", "\\bibitem",
        "\\NewDocumentCommand",
        "\\ProvideDocumentCommand",
        "\\DeclareDocumentCommand",
        "\\NewDocumentEnvironment",
        "\\ProvideDocumentEnvironment",
        "\\DeclareDocumentEnvironment"
    )

    /**
     * Commands that should not have the given file extensions.
     */
    val illegalExtensions = mapOf(
        INPUT.cmd to listOf(".tex"),
        INCLUDE.cmd to listOf(".tex"),
        SUBFILEINCLUDE.cmd to listOf(".tex"),
        BIBLIOGRAPHY.cmd to listOf(".bib"),
        INCLUDEGRAPHICS.cmd to FileMagic.graphicFileExtensions.map { ".$it" }, // https://tex.stackexchange.com/a/1075/98850
        USEPACKAGE.cmd to listOf(".sty"),
        EXTERNALDOCUMENT.cmd to listOf(".tex"),
    )

    /**
     * Commands which can include packages in optional or required arguments.
     */
    val packageInclusionCommands = setOf(
        USEPACKAGE, REQUIREPACKAGE, DOCUMENTCLASS, LOADCLASS
    ).map { it.cmd }.toSet()

    val tikzLibraryInclusionCommands = setOf(USETIKZLIBRARY.cmd)

    val pgfplotsLibraryInclusionCommands = setOf(USEPGFPLOTSLIBRARY.cmd)

    /**
     * Commands that should have the given file extensions.
     */
    val requiredExtensions = mapOf(
        ADDBIBRESOURCE.cmd to listOf("bib")
    )

    /**
     * Extensions that should only be scanned for the provided include commands.
     */
    val includeOnlyExtensions: Map<String, Set<String>> = mapOf(
        INCLUDE.cmd to hashSetOf("tex"),
        INCLUDEONLY.cmd to hashSetOf("tex"),
        SUBFILE.cmd to hashSetOf("tex"),
        SUBFILEINCLUDE.cmd to hashSetOf("tex"),
        BIBLIOGRAPHY.cmd to hashSetOf("bib"),
        ADDBIBRESOURCE.cmd to hashSetOf("bib"),
        REQUIREPACKAGE.cmd to hashSetOf("sty"),
        USEPACKAGE.cmd to hashSetOf("sty"),
        DOCUMENTCLASS.cmd to hashSetOf("cls"),
        LOADCLASS.cmd to hashSetOf("cls"),
        EXTERNALDOCUMENT.cmd to hashSetOf("tex") // Not completely true, as it only includes labels. Technically it references an aux file
    )

    /**
     * Commands that include bib files.
     */
    val bibliographyIncludeCommands = includeOnlyExtensions.entries.filter { it.value.contains("bib") }.map { it.key }

    @Suppress("unused")
    val startIfs = hashSetOf(
        IF, IFCAT, IFX,
        IFCASE, IFNUM, IFODD,
        IFHMODE, IFVMODE, IFMMODE,
        IFINNER, IFDIM, IFVOID,
        IFHBOX, IFVBOX, IFEOF,
        IFTRUE, IFFALSE
    ).map { it.cmd }

    /**
     * All commands that end if.
     */
    val endIfs = hashSetOf(FI.cmd)

    /**
     * All commands that at first glance look like \if-esque commands, but that actually aren't.
     */
    val ignoredIfs = hashSetOf("\\newif", "\\iff", "\\ifthenelse", "\\iftoggle", "\\ifoot", "\\ifcsvstrcmp")

    /**
     * List of all TeX style primitives.
     */
    val stylePrimitives = listOf(
        RM.cmd, SF.cmd, TT.cmd, IT.cmd, SL.cmd, SC.cmd, BF.cmd
    )

    /**
     * The LaTeX counterparts of all [stylePrimitives] commands.
     */
    val stylePrimitiveReplacements = mapOf(
        RM.cmd to "\\textrm", SF.cmd to "\\textsf", TT.cmd to "\\texttt", IT.cmd to "\\textit",
        SL.cmd to "\\textsl", SC.cmd to "\\textsc", BF.cmd to "\\textbf"
    )

    /**
     * Set of text styling commands
     */
    val textStyles = setOf(
        TEXTRM, TEXTSF, TEXTTT, TEXTIT,
        TEXTSL, TEXTSC, TEXTBF, EMPH,
        TEXTUP, TEXTMD
    ).map { it.cmd }

    /**
     * All LaTeX commands that contain a url (in their first parameter).
     */
    val urls = hashSetOf(URL.cmd, HREF.cmd)

    /**
     * All BibTeX tags that take a url as their parameter.
     */
    val bibUrls = hashSetOf("url", "biburl")

    /**
     * Commands that always contain a certain language.
     *
     * Maps the name of the command to the registered Language id.
     */
    val languageInjections = hashMapOf(
        DIRECTLUA.cmd to "Lua",
        LUAEXEC.cmd to "Lua"
    )

    /**
     * Commands that have a verbatim argument.
     *
     * Maps a command to a boolean that is true when the required argument can be specified with any pair of characters.
     */
    val verbatim = hashMapOf(
        "verb" to true,
        "verb*" to true,
        "directlua" to false,
        "luaexec" to false,
        "lstinline" to true
    )

    /**
     *
     */
    val foldableFootnotes = listOf(
        FOOTNOTE.cmd, FOOTCITE.cmd
    )
}

package nl.hannahsten.texifyidea.util.magic

/**
 * @author Abby Berkers
 */
object CommentMagic {

    val preambleValues = hashSetOf("tikz", "math")

    val fakeSectionValues = hashSetOf("part", "chapter", "section", "subsection", "subsubsection", "paragraph", "subparagraph")
}

package nl.hannahsten.texifyidea.util.magic

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.lang.DefaultEnvironment.*
import nl.hannahsten.texifyidea.lang.alias.EnvironmentManager

object EnvironmentMagic {

    val listingEnvironments = hashSetOf(ITEMIZE, ENUMERATE, DESCRIPTION).map { it.env }

    private val tableEnvironmentsWithoutCustomEnvironments = hashSetOf(TABULAR, TABULAR_STAR, TABULARX, TABULARY, ARRAY, LONGTABLE, TABU, MATRIX, BMATRIX, PMATRIX, VMATRIX, VMATRIX_CAPITAL, WIDETABULAR, BLOCKARRAY, BLOCK, TBLR, LONGTBLR, TALLTBLR).map { it.env }

    /**
     * Get all table environments in the project, including any user defined aliases.
     */
    fun getAllTableEnvironments(project: Project): Set<String> {
        EnvironmentManager.updateAliases(tableEnvironmentsWithoutCustomEnvironments, project)
        return EnvironmentManager.getAliases(tableEnvironmentsWithoutCustomEnvironments.first())
    }

    /**
     * Environments that define their label via an optional parameter
     */
    val labelAsParameter = hashSetOf(LISTINGS.env, VERBATIM_CAPITAL.env)

    /**
     * Environments that introduce figures
     */
    val figures = hashSetOf(FIGURE.env)

    // Note: used in the lexer
    @JvmField
    val verbatim = hashSetOf(
        VERBATIM.env, VERBATIM_CAPITAL.env, LISTINGS.env, "plantuml", LUACODE.env, LUACODE_STAR.env, PYCODE.env,
        "sagesilent", "sageblock", "sagecommandline", "sageverbatim", "sageexample", "minted"
    )

    /**
     * Do a guess whether the environment is a verbatim environment.
     * Note: used in the lexer, so it should be fast.
     */
    @JvmStatic
    fun isProbablyVerbatim(environmentName: String): Boolean {
        // It might use \newminted environments, which always end in code
        // There are other environments that have 'code' in their name, if they are all verbatim environments is unclear
        // See https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/2847#issuecomment-1347941386
        return verbatim.contains(environmentName) || environmentName.endsWith("code")
    }

    /**
     * Environments that always contain a certain language.
     *
     * Maps the name of the environment to the registered Language id.
     */
    val languageInjections = hashMapOf(
        LUACODE.env to "Lua",
        LUACODE_STAR.env to "Lua"
    )

    val algorithmEnvironments = setOf(ALGORITHMIC.env)

    /**
     * All environments that define a matrix.
     */
    val matrixEnvironments = setOf(
        "matrix", "pmatrix", "bmatrix", "vmatrix", "Bmatrix", "Vmatrix",
        "matrix*", "pmatrix*", "bmatrix*", "vmatrix*", "Bmatrix*", "Vmatrix*",
        "smallmatrix", "psmallmatrix", "bsmallmatrix", "vsmallmatrix", "Bsmallmatrix", "Vsmallmatrix",
        "smallmatrix*", "psmallmatrix*", "bsmallmatrix*", "vsmallmatrix*", "Bsmallmatrix*", "Vsmallmatrix*",
        "gmatrix", "tikz-cd"
    )

    val alignableEnvironments = setOf(
        "eqnarray", "eqnarray*",
        "split",
        "align", "align*",
        "alignat", "alignat*",
        "flalign", "flalign*",
        "aligned", "alignedat",
        "cases", "dcases"
    ) + matrixEnvironments
}

package nl.hannahsten.texifyidea.util.magic

import nl.hannahsten.texifyidea.file.*
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.BIBLIOGRAPHY
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.INCLUDE

object FileMagic {

    /**
     * All file extensions of files that can be included (and where the included files contain language that needs to be considered).
     */
    val includeExtensions = hashSetOf("tex", "sty", "cls", "bib")

    val automaticExtensions = mapOf(
        INCLUDE.cmd to LatexFileType.defaultExtension,
        BIBLIOGRAPHY.cmd to BibtexFileType.defaultExtension
    )

    /**
     * All possible file types.
     */
    val fileTypes = setOf(
        LatexFileType,
        StyleFileType,
        ClassFileType,
        BibtexFileType,
        TikzFileType
    )

    /**
     * All file extensions that have to be deleted when clearing auxiliary files.
     *
     * This list is the union of @generated_exts in latexmk and the defined Auxiliary files in TeXWorks, plus some additions.
     * (https://github.com/TeXworks/texworks/blob/9c8cc8b88505103cb8f43fe4105638c77c7e7303/res/resfiles/configuration/texworks-config.txt#L37).
     */
    val auxiliaryFileTypes = arrayOf("aux", "bbl", "bcf", "brf", "fls", "idx", "ind", "lof", "lot", "nav", "out", "snm", "toc", "glo", "gls", "ist", "xdy")

    /**
     * All file extensions that are probably generated by LaTeX and some common packages.
     * Because any package can generated new files, this list is not complete.
     */
    // Actually run.xml should be included (latexmk) but file extensions with a dot are not found currently, see Utils#File.extension
    val generatedFileTypes = auxiliaryFileTypes + arrayOf("blg", "dvi", "fdb_latexmk", "ilg", "log", "out.ps", "pdf", "xml", "sagetex.sage", "sagetex.scmd", "sagetex.sout", "synctex", "gz", "synctex(busy)", "upa", "doctest.sage", "xdv", "glg", "glstex")

    /**
     * All bibtex keys which have a reference to a (local) file in the content.
     */
    val bibtexFileKeys = setOf("bibsource", "file")

    /**
     * Extensions of index-related files, which are generated by makeindex-like programs and should be copied next to the main file for the index/glossary packages to work.
     */
    val indexFileExtensions = setOf("ind", "glo", "ist", "xdy")

    /**
     * Extensions of files required by bib2gls
     */
    val bib2glsDependenciesExtensions = setOf("aux", "glg", "log")

    /**
     * All extensions for graphic files.
     */
    val graphicFileExtensions = setOf("eps", "pdf", "png", "jpeg", "jpg", "jbig2", "jp2")
}

package nl.hannahsten.texifyidea.util.magic

import com.intellij.codeInspection.LocalQuickFix
import org.intellij.lang.annotations.Language

object GeneralMagic {

    val noQuickFix: LocalQuickFix? = null

    val latexDemoText =
        """
            |%
            |% An amazing example for LaTeX.
            |%
            |\documentclass[<optionalParam>12pt,a4paper</optionalParam>]{article}
            |
            |% Package imports.
            |\usepackage{amsmath}
            |\usepackage{listings}
            |
            |% Document wide TikZ settings.
            |\tikzset{
            |   mystyle/.style={
            |        draw,
            |        circle,
            |        label={[fill=yellow]0:#1}
            |    }
            |}
            |
            |% Very incomplete Kotlin definition for listings.
            |\lstdefinelanguage{Kotlin}{
            |basicstyle={\ttfamily},
            |    keywords={fun, if, else, this, let},
            |    keywordstyle={\color{orange!80!red}},
            |}
            |
            |% Define title.
            |\title{A Very Simple \LaTeXe{} Template}
            |\author{
            |        Henk-Jan\\Department of YUROP\\University of Cheese\\
            |        Windmill City, 2198 AL, \underline{Tulipa}
            |}
            |\date{\today}
            |
            |% Start document.
            |\begin{document}
            |    \maketitle
            |
            |    % Start writing amazing stuff now.
            |    \begin{abstract}
            |        This is the paper's abstract.
            |        In this paper, we do basically nothing.
            |    \end{abstract}
            |
            |    \section{Introduction}\label{sec:introduction}
            |    This is time for all good women to come to the aid of their party!
            |
            |    \section{Mathematics}\label{sec:mathematics}
            |    To start the party, as ``announced'' in Section~\ref{sec:introduction}, please take a look at the value of <inlineMath>${'$'}x <inlineCommand>\times</inlineCommand>
            |    <inlineCommand>\frac</inlineCommand>{5}{<inlineCommand>\sqrt</inlineCommand>{3}}${'$'}</inlineMath> in the following equation:
            |    <displayMath>\[
            |       x <displayCommand>\times</displayCommand> <displayCommand>\frac</displayCommand>{5}{<displayCommand>\sqrt</displayCommand>{3}} = y <displayCommand>\cdot</displayCommand> <displayCommand>\max\left</displayCommand>{ 4, <displayCommand>\alpha</displayCommand>, 6 <displayCommand>\right</displayCommand>} +
            |           <displayCommand>\sqrt</displayCommand>[<optionalParam>1234</optionalParam>]{5678}.
            |    \]</displayMath>
            |
            |    \paragraph{Programming}
            |    % @formatter:off
            |    \begin{lstlisting}[language=Kotlin]
            |fun Int?.ifPositiveAddTwo(): Int =
            |        this?.let {
            |            if (this >= 0) this + 2
            |            else this
            |        } ?: 0
            |    \end{lstlisting}
            |    % @formatter:on
            |
            |    \subsection{More work}\label{subsec:moreWork}
            |    A much longer \LaTeXe{} example was written by Henk-Jan~\cite{Gil:02}.
            |    But we can also just do some more epic plugin showoffy stuff like
            |    <displayMath>\begin{align}
            |       <displayCommand>\text</displayCommand>{Stuff here is also highlighted, and also }
            |       <displayCommand>\sum</displayCommand>_{i=0}^n <displayCommand>\left</displayCommand>( i <displayCommand>\right</displayCommand>)
            |    \end{align}</displayMath>
            |
            |    \section{Results}\label{sec:results}
            |    In this section we describe the results.
            |    So basically <inlineMath>${'$'}x${'$'}</inlineMath> but maybe also <inlineMath>${'$'}<inlineCommand>\hat</inlineCommand>{x}^{2y}${'$'}</inlineMath>.
            |
            |    \section{Conclusions}\label{sec:conclusions}
            |    We worked hard, and achieved very little.
            |    Or did we?
            |
            |    % Another extremely descriptive comment.
            |    \bibliographystyle{abbrv}
            |    \bibliography{main}
            |
            |\end{document}
        """.trimMargin()

    @Language("Bibtex")
    val bibtexDemoText =
        """
            % I am a BibTeX comment.
            @article{greenwade1993,
                author  = "George D. Greenwade",
                title   = "The {C}omprehensive {T}ex {A}rchive {N}etwork ({CTAN})",
                year    = "1993",
                journal = "TUGBoat",
                volume  = "14",
                number  = "3",
                pages   = "342--351"
            }

            I am also a BibTeX comment.
            @book{goossens1993,
                author    = "Michel Goossens and Frank Mittelbach and Alexander Samarin",
                title     = "The LaTeX Companion",
                year      = "1993",
                publisher = "Addison-Wesley",
                address   = "Reading, Massachusetts"
            }
        """.trimIndent()
}

package nl.hannahsten.texifyidea.util.magic

import nl.hannahsten.texifyidea.TexifyIcons

object IconMagic {

    /**
     * Maps file extentions to their corresponding icons.
     */
    val fileIcons = mapOf(
        "pdf" to TexifyIcons.PDF_FILE,
        "dvi" to TexifyIcons.DVI_FILE,
        "synctex.gz" to TexifyIcons.SYNCTEX_FILE,
        "bbl" to TexifyIcons.BBL_FILE,
        "aux" to TexifyIcons.AUX_FILE,
        "tmp" to TexifyIcons.TEMP_FILE,
        "dtx" to TexifyIcons.DOCUMENTED_LATEX_SOURCE,
        "bib" to TexifyIcons.BIBLIOGRAPHY_FILE,
        "toc" to TexifyIcons.TABLE_OF_CONTENTS_FILE,
        "tikz" to TexifyIcons.TIKZ_FILE
    )
}

package nl.hannahsten.texifyidea.util.magic

import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.lang.commands.LatexCommand

/**
 * Saves typing.
 */
internal val LatexCommand.cmd: String
    get() = this.commandWithSlash

/**
 * Saves typing.
 */
internal val Environment.env: String
    get() = this.environmentName

package nl.hannahsten.texifyidea.util.magic

import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.HVINDEX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.IDXLAYOUT
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.IMAKEIDX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.INDEX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.INDEXTOOLS
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.MAKEIDX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.MULTIND
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.REPEATINDEX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.SPLITIDX
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.SPLITINDEX

object PackageMagic {

    /**
     * All unicode enabling packages.
     */
    val unicode = hashSetOf(
        LatexPackage.INPUTENC.with("utf8"),
        LatexPackage.FONTENC.with("T1")
    )

    /**
     * All known packages which provide an index.
     */
    val index = hashSetOf(
        MAKEIDX, MULTIND, INDEX, SPLITIDX, SPLITINDEX, IMAKEIDX, HVINDEX, IDXLAYOUT, REPEATINDEX, INDEXTOOLS
    )

    /**
     * Packages which provide a glossary.
     */
    val glossary = hashSetOf(LatexPackage.GLOSSARIES, LatexPackage.GLOSSARIESEXTRA)

    /**
     * Known conflicting packages.
     */
    val conflictingPackages = listOf(
        setOf(LatexPackage.BIBLATEX, LatexPackage.NATBIB)
    )

    /**
     * Maps packages to the packages it loads.
     * Note that when a LaTeX SDK is available, then the relative inclusions are handled for all installed packages by [nl.hannahsten.texifyidea.index.file.LatexExternalPackageInclusionCache].
     * This list is just there as a sort of default for those users who do not have LaTeX packages installed for example.
     */
    val packagesLoadingOtherPackages: Map<LatexPackage, Set<LatexPackage>> = mapOf(
        LatexPackage.AMSSYMB to setOf(LatexPackage.AMSFONTS),
        LatexPackage.MATHTOOLS to setOf(LatexPackage.AMSMATH),
        LatexPackage.GRAPHICX to setOf(LatexPackage.GRAPHICS),
        LatexPackage.XCOLOR to setOf(LatexPackage.COLOR),
        LatexPackage.PDFCOMMENT to setOf(LatexPackage.HYPERREF),
        LatexPackage.ALGORITHM2E to setOf(LatexPackage.ALGPSEUDOCODE), // Not true, but algorithm2e provides roughly the same commands
        LatexPackage.NEWTXMATH to setOf(LatexPackage.AMSSYMB, LatexPackage.STMARYRD), // Not true, but newtxmath provides roughly the same commands
    )

    /**
     * Maps argument specifiers to whether they are required (true) or
     * optional (false).
     */
    val xparseParamSpecifiers = mapOf(
        'm' to true,
        'r' to true,
        'R' to true,
        'v' to true,
        'b' to true,
        'o' to false,
        'd' to false,
        'O' to false,
        'D' to false,
        's' to false,
        't' to false,
        'e' to false,
        'E' to false
    )
}

package nl.hannahsten.texifyidea.util.magic

import nl.hannahsten.texifyidea.inspections.latex.codestyle.LatexLineBreakInspection
import java.util.regex.Pattern

typealias RegexPattern = Pattern

/**
 * @author Hannah Schellekens
 */
object PatternMagic {

    val ellipsis = Regex("""(?<!\.)(\.\.\.)(?!\.)""")

    /**
     * This is the only correct way of using en dashes.
     */
    val correctEnDash = RegexPattern.compile("[0-9]+--[0-9]+")!!

    /**
     * Matches the prefix of a label. Amazing comment this is right?
     */
    val labelPrefix = RegexPattern.compile(".*:")!!

    /**
     * Matches the end of a sentence.
     *
     * Includes `[^.][^.]` because of abbreviations (at least in Dutch) like `s.v.p.`
     * An English semicolon is not a sentence end, but a greek question mark is.
     */
    const val sentenceEndPrefix = "[^.A-Z][^.A-Z]"
    val sentenceEnd = RegexPattern.compile("($sentenceEndPrefix[.?!;] +[^%\\s])|(^\\. )")!!

    /**
     * Matches all interpunction that marks the end of a sentence.
     */
    val sentenceSeparator = RegexPattern.compile("[.?!;;]")!!

    /**
     * Matches all sentenceSeparators at the end of a line (with or without space).
     */
    val sentenceSeparatorAtLineEnd = RegexPattern.compile("$sentenceSeparator\\s*$")!!

    /**
     * Matches when a string ends with a non breaking space.
     */
    val endsWithNonBreakingSpace = RegexPattern.compile("~$")!!

    /**
     * Finds all abbreviations that have at least two letters separated by comma's.
     *
     * It might be more parts, like `b.v.b.d. ` is a valid abbreviation. Likewise are `sajdflkj.asdkfj.asdf ` and
     * `i.e. `. Single period abbreviations are not being detected as they can easily be confused with two letter words
     * at the end of the sentence (also localisation...) For this there is a quickfix in [LatexLineBreakInspection].
     */
    val abbreviation = RegexPattern.compile("[0-9A-Za-z.]+\\.[A-Za-z](\\.[\\s~])")!!

    /**
     * Abbreviations not detected by [PatternMagic.abbreviation].
     */
    val unRegexableAbbreviations = listOf(
        "et al."
    )

    /** [abbreviation]s that are missing a normal space (or a non-breaking space) */
    val abbreviationWithoutNormalSpace = RegexPattern.compile("[0-9A-Za-z.]+\\.[A-Za-z](\\.\\s)")!!

    /**
     * Matches all comments, starting with % and ending with a newline.
     */
    val comments: RegexPattern = RegexPattern.compile("%(.*)\\n")

    /**
     * Matches leading and trailing whitespace on a string.
     */
    val excessWhitespace = RegexPattern.compile("(^(\\s+).*(\\s*)\$)|(^(\\s*).*(\\s+)\$)")!!

    /**
     * Matches a non-ASCII character.
     */
    val nonAscii = RegexPattern.compile("\\P{ASCII}")!!

    /**
     * Separator for multiple parameter values in one parameter.
     *
     * E.g. when you have \cite{citation1,citation2,citation3}, this pattern will match the separating
     * comma.
     */
    val parameterSplit = RegexPattern.compile("\\s*,\\s*")!!

    /**
     * Matches the extension of a file name.
     */
    val fileExtension = RegexPattern.compile("\\.[a-zA-Z0-9]+$")!!

    /**
     * Matches all leading whitespace.
     */
    val leadingWhitespace = RegexPattern.compile("^\\s*")!!

    /**
     * Matches newlines.
     */
    val newline = RegexPattern.compile("\\n")!!

    /**
     * Checks if the string is `text`, two newlines, `text`.
     */
    val containsMultipleNewlines = RegexPattern.compile("[^\\n]*\\n\\n+[^\\n]*")!!

    /**
     * Matches HTML tags of the form `<tag>`, `<tag/>` and `</tag>`.
     */
    val htmlTag = RegexPattern.compile("""<.*?/?>""")!!

    /**
     * Matches a LaTeX command that is the start of an \if-\fi structure.
     */
    val ifCommand = RegexPattern.compile("\\\\if[a-zA-Z@]*")!!

    /**
     * Matches the begin and end commands of the cases and split environments.
     */
    val casesOrSplitCommands = Regex(
        "((?=\\\\begin\\{cases})|(?<=\\\\begin\\{cases}))" +
            "|((?=\\\\end\\{cases})|(?<=\\\\end\\{cases}))" +
            "|((?=\\\\begin\\{split})|(?<=\\\\begin\\{split}))" +
            "|((?=\\\\end\\{split})|(?<=\\\\end\\{split}))"
    )

    /**
     * Matches any consecutive sequence of LaTeX quote characters
     */
    val quotePattern = """["'`]+""".toRegex()
}

package nl.hannahsten.texifyidea.util.magic

import nl.hannahsten.texifyidea.util.mapOfVarargs

object TypographyMagic {

    /**
     * Matches each (supported) opening brace to it's opposite close brace.
     */
    @JvmField
    val braceOpposites = mapOfVarargs(
        "(", ")",
        "[", "]",
        "\\{", "\\}",
        "<", ">",
        "|", "|",
        "\\|", "\\|"
    )

    /**
     * Algorithmicx pairs (also hardcoded in lexer).
     */
    val pseudoCodeBeginEndOpposites = mapOf(
        "If" to "EndIf",
        "For" to "EndFor",
        "ForAll" to "EndFor",
        "While" to "EndWhile",
        "Repeat" to "Until",
        "Loop" to "EndLoop",
        "Function" to "EndFunction",
        "Procedure" to "EndProcedure"
    )
}

package nl.hannahsten.texifyidea.util.parser

import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexId
import nl.hannahsten.texifyidea.util.evaluate

fun BibtexEntry.getTitle(): String {
    val stub = this.stub
    return stub?.title ?: this.getTagContent("title")
}

fun BibtexEntry.getAuthors(): List<String> {
    val stub = this.stub
    if (stub != null) return stub.authors.filterNotNull()
    val authorList = this.getTagContent("author")
    return listOf(*authorList.split(" and ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray())
}

fun BibtexEntry.getYear(): String {
    val stub = this.stub
    return stub?.year ?: this.getTagContent("year")
}

fun BibtexEntry.getIdentifier(): String {
    val stub = this.stub
    return stub?.identifier ?: (firstChildOfType(BibtexId::class)?.text ?: return "")
}

fun BibtexEntry.getAbstract(): String {
    return this.getTagContent("abstract")
}

fun BibtexEntry.getTagContent(tagName: String?): String {
    val entryContent = this.entryContent ?: return ""

    entryContent.tagList.forEach {
        if (tagName.equals(it.key.text, ignoreCase = true)) {
            val text = it.content?.evaluate() ?: return ""

            // Deal with braced strings.
            return if (text.first() == '{' && text.last() == '}') {
                text.substring(1, text.length - 1)
            }
            else text
        }
    }

    return ""
}

package nl.hannahsten.texifyidea.util.parser

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.paths.WebReference
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.SUBFILES
import nl.hannahsten.texifyidea.lang.commands.*
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.reference.LatexLabelReference
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.shrink
import java.util.regex.Pattern
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set

/**
 * Check if the command includes other files, and if so return [InputFileReference] instances for them.
 *
 * Use this instead of command.references.filterIsInstance<InputFileReference>(), to avoid resolving references of types that will not be needed.
 */
fun LatexCommands.getFileArgumentsReferences(): List<InputFileReference> {
    val inputFileReferences = mutableListOf<InputFileReference>()

    // There may be multiple commands with this name, just guess the first one
    val command = LatexCommand.lookup(this.name)?.firstOrNull() ?: return emptyList()

    // Arguments from the LatexCommand (so the command as hardcoded in e.g. LatexRegularCommand)
    val requiredArguments = command.arguments.mapNotNull { it as? RequiredArgument }

    // Find file references within required parameters and across required parameters (think \referencing{reference1,reference2}{reference3} )
    for (i in requiredParameters().indices) {
        // Find the corresponding requiredArgument
        val requiredArgument = if (i < requiredArguments.size) requiredArguments[i] else requiredArguments.lastOrNull { it is RequiredFileArgument } ?: continue

        // Check if the actual argument is a file argument or continue with the next argument
        val fileArgument = requiredArgument as? RequiredFileArgument ?: continue
        val extensions = fileArgument.supportedExtensions

        // Find text range of parameters, relative to command startoffset
        val requiredParameter = requiredParameters()[i]
        val subParamRanges = if (requiredArgument.commaSeparatesArguments) {
            extractSubParameterRanges(requiredParameter).map {
                it.shiftRight(requiredParameter.textOffset - this.textOffset)
            }
        }
        else {
            listOf(requiredParameter.textRange.shrink(1).shiftLeft(this.textOffset))
        }

        for (subParamRange in subParamRanges) {
            inputFileReferences.add(InputFileReference(this, subParamRange, extensions, fileArgument.defaultExtension))
        }
    }

    // Special case for the subfiles package: the (only) mandatory optional parameter should be a path to the main file
    // We reference it because we include the preamble of that file, so it is in the file set (partially)
    if (name == LatexGenericRegularCommand.DOCUMENTCLASS.cmd && SUBFILES.name in getRequiredParameters() && getOptionalParameterMap().isNotEmpty()) {
        val range = this.firstChildOfType(LatexParameter::class)?.textRangeInParent
        if (range != null) {
            inputFileReferences.add(InputFileReference(this, range.shrink(1), setOf("tex"), "tex"))
        }
    }

    return inputFileReferences
}

/**
 * Create label references from the command parameter given, assuming it is a known command with label referencing parameters.
 */
fun extractLabelReferences(element: LatexCommands, requiredParameters: List<LatexRequiredParam>): List<PsiReference> {
    // Assume that any possible label reference is a required parameter
    val defaultParameter = requiredParameters.getOrNull(0) ?: return emptyList()

    // Find the command parameters which are a label reference
    return (
        LatexCommand.lookup(element.name)
            ?.firstOrNull()
            ?.arguments
            ?.withIndex()
            ?.filter { it.value.type == Argument.Type.LABEL }
            // Use the known parameter indices to match with the actual parameters
            ?.mapNotNull { requiredParameters.getOrNull(it.index) }
            ?.ifEmpty { listOf(defaultParameter) }
            ?: listOf(defaultParameter)
        )
        .flatMap { param ->
            extractSubParameterRanges(param).map { range ->
                LatexLabelReference(
                    element,
                    range.shiftRight(param.textOffset - element.textOffset)
                )
            }
        }
}

fun getRequiredParameters(element: LatexCommands): List<LatexRequiredParam> {
    return ApplicationManager.getApplication().runReadAction(
        Computable {
            element.requiredParameters()
        }
    )
}

fun extractSubParameterRanges(param: LatexRequiredParam): List<TextRange> {
    return splitToRanges(stripGroup(param.text), PatternMagic.parameterSplit)
        .map { r: TextRange -> r.shiftRight(1) }
}

fun splitToRanges(text: String, pattern: Pattern): List<TextRange> {
    val parts = pattern.split(text)
    val ranges: MutableList<TextRange> = ArrayList()
    var currentOffset = 0
    for (part in parts) {
        val partStartOffset = text.indexOf(part, currentOffset)
        ranges.add(TextRange.from(partStartOffset, part.length))
        currentOffset = partStartOffset + part.length
    }
    return ranges
}

fun stripGroup(text: String): String {
    if (text.length < 2) return ""
    return text.substring(1, text.length - 1)
}

/**
 * Generates a map of parameter names and values (assuming they are in the form name=value) for all optional parameters, comma-separated and separate optional parameters are treated equally.
 * If a value does not have a name, the value will be the key in the hashmap mapping to the empty string.
 */
// Explicitly use a LinkedHashMap to preserve iteration order
fun<K : PsiElement, V : PsiElement> Map<K, V?>.toStringMap(): LinkedHashMap<String, String> {
    val parameterMap = LinkedHashMap<String, String>()
    this.forEach { (k, v) -> parameterMap[k.toString()] = v?.toString() ?: "" }
    return parameterMap
}

fun getOptionalParameterMapFromParameters(parameters: List<LatexParameter>): LinkedHashMap<LatexOptionalKeyValKey, LatexKeyValValue?> {
    val parameterMap = LinkedHashMap<LatexOptionalKeyValKey, LatexKeyValValue?>()
    // Parameters can be defined using multiple optional parameters, like \command[opt1][opt2]{req1}
    // But within a parameter, there can be different content like [name={value in group}]
    parameters.mapNotNull { it.optionalParam }
        // extract the content of each parameter element
        .flatMap { param ->
            param.optionalKeyValPairList
        }.forEach { pair ->
            parameterMap[pair.optionalKeyValKey] = pair.keyValValue
        }
    return parameterMap
}

fun getRequiredParameters(parameters: List<LatexParameter>): List<String> {
    return parameters.mapNotNull { it.requiredParam }
        .map { param ->
            param.text.dropWhile { it == '{' }.dropLastWhile { it == '}' }.trim()
        }
}

fun LatexCommands.extractUrlReferences(firstParam: LatexRequiredParam): Array<PsiReference> =
    extractSubParameterRanges(firstParam)
        .map { WebReference(this, it.shiftRight(firstParam.textOffset - textOffset)) }
        .toArray(emptyArray())

package nl.hannahsten.texifyidea.util.parser

import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.nextLeaf
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.lang.commands.LatexMathCommand
import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand
import nl.hannahsten.texifyidea.lang.commands.OptionalArgument
import nl.hannahsten.texifyidea.lang.commands.RequiredArgument
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.lineIndentation
import nl.hannahsten.texifyidea.util.magic.ColorMagic
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import kotlin.math.min

/**
 * Checks whether the given LaTeX commands is a definition or not.
 *
 * This is either a command definition or an environment definition. Does not count redefinitions.
 *
 * @return `true` if the command is an environment definition or a command definition, `false` when the command is
 *         `null` or otherwise.
 */
fun LatexCommands?.isDefinition() = this != null && this.name in CommandMagic.definitions

/**
 * Checks whether the given LaTeX commands is a color definition or not.
 *
 * @return `true` if the command defines a color, `false` when the command command
 *          is `null` or otherwise.
 */
fun LatexCommands?.isColorDefinition() = this != null && this.name?.substring(1) in ColorMagic.colorDefinitions.map { it.command }

fun LatexCommands?.usesColor() = this != null && this.name?.substring(1) in ColorMagic.colorCommands

/**
 * Checks whether the given LaTeX commands is a (re)definition or not.
 *
 * This is either a command definition or an environment (re)definition.
 *
 * @return `true` if the command is an environment (re)definition or a command (re)definition, `false` when the command is
 *         `null` or otherwise.
 */
fun LatexCommands?.isDefinitionOrRedefinition() = this != null &&
    (
        this.name in CommandMagic.commandDefinitionsAndRedefinitions ||
            this.name in CommandMagic.environmentDefinitions || this.name in CommandMagic.environmentRedefinitions
        )

/**
 * Checks whether the given LaTeX commands is a command definition or not.
 *
 * @return `true` if the command is a command definition, `false` when the command is `null` or otherwise.
 */
fun LatexCommands?.isCommandDefinition(): Boolean = this != null && name in CommandMagic.commandDefinitionsAndRedefinitions

/**
 * Checks whether the given LaTeX commands is an environment definition or not.
 *
 * @return `true` if the command is an environment definition, `false` when the command is `null` or otherwise.
 */
fun LatexCommands?.isEnvironmentDefinition(): Boolean = this != null && name in CommandMagic.environmentDefinitions

/**
 * Get the command that gets defined by a definition (`\let` or `\def` command).
 */
fun LatexCommands.definitionCommand(): LatexCommands? = forcedFirstRequiredParameterAsCommand()

/**
 * Get the name of the command that is defined by `this` command.
 */
fun LatexCommands.definedCommandName() = definitionCommand()?.name

/**
 * Checks whether the command has a star or not.
 */
fun LatexCommands.hasStar() = childrenOfType(LeafPsiElement::class).any {
    it.elementType == LatexTypes.STAR
}

/**
 * Looks for the next command relative to the given command.
 *
 * @return The next command in the file, or `null` when there is no such command.
 */
fun LatexCommands.nextCommand(): LatexCommands? {
    val content = parentOfType(LatexNoMathContent::class) ?: return null
    val next = content.nextSiblingIgnoreWhitespace() as? LatexNoMathContent
        ?: return null
    return next.firstChildOfType(LatexCommands::class)
}

/**
 * Looks for the previous command relative to the given command.
 *
 * @return The previous command in the file, or `null` when there is no such command.
 */
fun LatexCommands.previousCommand(): LatexCommands? {
    val content = parentOfType(LatexNoMathContent::class) ?: return null
    val previous = content.previousSiblingIgnoreWhitespace() as? LatexNoMathContent
        ?: return null
    return previous.firstChildOfType(LatexCommands::class)
}

/**
 * Get the value of the named [argument] given in `this` command.
 */
fun LatexCommands.getRequiredArgumentValueByName(argument: String): String? {
    // Find all pre-defined commands that define `this` command.
    val requiredArgIndices = LatexRegularCommand[
        name?.substring(1)
            ?: return null
    ]
        // Find the index of their required parameter named [argument].
        ?.map {
            it.arguments.filterIsInstance<RequiredArgument>()
                .indexOfFirst { arg -> arg.name == argument }
        }
    return if (requiredArgIndices.isNullOrEmpty() || requiredArgIndices.all { it == -1 }) null
    else getRequiredParameters().getOrNull(min(requiredArgIndices.first(), getRequiredParameters().size - 1))
}

/**
 * Get the value of the named optional [argument] given in `this` command.
 *
 * @return null when the optional argument is not given.
 */
fun LatexCommands.getOptionalArgumentValueByName(argument: String): String? {
    // Find all pre-defined commands that define `this` command.
    val optionalArgIndices = LatexRegularCommand[
        name?.substring(1)
            ?: return null
    ]
        // Find the index of their optional argument named [argument].
        ?.map {
            it.arguments.filterIsInstance<OptionalArgument>()
                .indexOfFirst { arg -> arg.name == argument }
        }
    return if (optionalArgIndices.isNullOrEmpty() || optionalArgIndices.all { it == -1 }) null
    else getOptionalParameterMap().keys.toList().getOrNull(min(optionalArgIndices.first(), getOptionalParameterMap().keys.toList().size - 1))?.text
}

/**
 * Checks whether the command is known by TeXiFy.
 *
 * @return Whether the command is known (`true`), or unknown (`false`).
 */
fun LatexCommands.isKnown(): Boolean {
    val name = name?.substring(1) ?: ""
    return LatexRegularCommand[name] != null || LatexMathCommand[name] != null
}

/**
 * Get the text contents of the `index+1`th required parameter of the command.
 *
 * @throws IllegalArgumentException When the index is negative.
 */
@Throws(IllegalArgumentException::class)
fun LatexCommands.requiredParameter(index: Int): String? {
    require(index >= 0) { "Index must not be negative" }

    val parameters = getRequiredParameters()
    if (parameters.isEmpty() || index >= parameters.size) {
        return null
    }

    return parameters[index]
}

/**
 * Finds the indentation of the line where the section command starts.
 */
fun LatexCommands.findIndentation(): String {
    val file = containingFile
    val document = file.document() ?: return ""
    val lineNumber = document.getLineNumber(textOffset)
    return document.lineIndentation(lineNumber)
}

/**
 * Get files included by this command.
 * @param includeInstalledPackages Whether to include a search for LaTeX packages installed on the system, if applicable for this command.
 */
fun LatexCommands.getIncludedFiles(includeInstalledPackages: Boolean): List<PsiFile> {
    return references.filterIsInstance<InputFileReference>()
        .mapNotNull { it.resolve(includeInstalledPackages) }
}

/**
 * Looks up all the required parameters of this command.
 *
 * @return A list of all required parameters.
 */
fun LatexCommands.requiredParameters(): List<LatexRequiredParam> = parameterList.asSequence()
    .filter { it.requiredParam != null }
    .mapNotNull(LatexParameter::getRequiredParam)
    .toList()

/**
 * Returns the forced first required parameter of a command as a command.
 *
 * This allows both example constructs `\\usepackage{\\foo}}` and `\\usepackage\\foo`,
 * which are equivalent.
 * Note that when the command does not take parameters this method might return untrue results.
 *
 * @return The forced first required parameter of the command.
 */
fun LatexCommands.forcedFirstRequiredParameterAsCommand(): LatexCommands? {
    val parameters = requiredParameters()
    if (parameters.isNotEmpty()) {
        val parameter = parameters.first()
        val found = PsiTreeUtil.findChildrenOfType(parameter, LatexCommands::class.java)
        return if (found.size == 1) found.first() else null
    }

    // This is just a bit of guesswork about the parser structure.
    // Probably, if we're looking at a \def\mycommand, if the sibling isn't it, probably the parent has a sibling.
    return nextSibling?.nextSiblingOfType(LatexCommands::class) ?: parent?.nextSiblingIgnoreWhitespace()?.firstChildOfType(LatexCommands::class)
}

/**
 * Checks if the command is followed by a label.
 */
fun LatexCommands.hasLabel(): Boolean {
    if (CommandMagic.labelAsParameter.contains(this.name)) {
        return getOptionalParameterMapFromParameters(this.parameterList).toStringMap().containsKey("label")
    }

    // Next leaf is a command token, parent is LatexCommands
    val labelMaybe = this.nextLeaf { it !is PsiWhiteSpace }?.parent as? LatexCommands ?: return false
    return CommandManager.labelAliasesInfo.getOrDefault(labelMaybe.commandToken.text, null)?.labelsPreviousCommand == true
}

/**
 * Get all [LatexCommands] that are children (direct or indirect) of the given element.
 */
fun PsiElement.allCommands() = childrenOfType(LatexCommands::class).toList()

package nl.hannahsten.texifyidea.util.parser

import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.files.commandsInFileSet

/**
 * Looks up the name of the environment in the required parameter.
 */
fun LatexEnvironment.name(): LatexParameterText? {
    return firstChildOfType(LatexParameterText::class)
}

/**
 * Checks if the environment contains the given context.
 */
fun LatexEnvironment.isContext(context: Environment.Context): Boolean {
    val name = name()?.text ?: return false
    val environment = Environment[name] ?: return false
    return environment.context == context
}

/**
 * Get the environment name of the begin command.
 */
fun LatexBeginCommand.environmentName(): String? = beginOrEndEnvironmentName(this)

/**
 * Finds the [LatexEndCommand] that matches the begin command.
 */
fun LatexBeginCommand.endCommand(): LatexEndCommand? = nextSiblingOfType(LatexEndCommand::class)

/**
 * Looks up all the required parameters from this begin command.
 *
 * @return A list of all required parameters.
 */
fun LatexBeginCommand.requiredParameters(): List<LatexRequiredParam> = parameterList.asSequence()
    .filter { it.requiredParam != null }
    .mapNotNull(LatexParameter::getRequiredParam)
    .toList()

/**
 * Checks if the given latex command marks a valid entry point for latex compilation.
 *
 * A valid entry point means that a latex compilation can start from the file containing the
 * given command.
 *
 * @return `true` if the command marks a valid entry point, `false` if not.
 */
fun LatexBeginCommand.isEntryPoint(): Boolean {
    // Currently: only allowing `\begin{document}`.
    val requiredParameters = requiredParameters()
    return requiredParameters.firstOrNull()?.text == "{document}"
}

/**
 * Get the environment name of the end command.
 */
fun LatexEndCommand.environmentName(): String? = beginOrEndEnvironmentName(this)

/**
 * Get the environment name of a begin/end command.
 *
 * @param element
 *              Either a [LatexBeginCommand] or a [LatexEndCommand]
 */
private fun beginOrEndEnvironmentName(element: PsiElement): String? = element.firstChildOfType(LatexParameterText::class)?.text

/**
 * Finds the [LatexBeginCommand] that matches the end command.
 */
fun LatexEndCommand.beginCommand(): LatexBeginCommand? = previousSiblingOfType(LatexBeginCommand::class)

/**
 * Checks if the latex content objects is a display math environment.
 */
fun LatexNoMathContent.isDisplayMath() = firstChildOfType(LatexDisplayMath::class) != null && firstChildOfType(LatexEnvironment::class) == null

/*
 * Technically it's impossible to determine for all cases whether a users wants to compile with biber or biblatex.
 * But often when people use the biblatex package they use biber.
 * And often, when they use biber they use \printbibliography instead of \bibliography.
 * Hence, the following methods often work - and if they don't, users can easily change the compiler in the run config.
 */

/**
 * Checks if the fileset for this file has a bibliography included.
 *
 * @return `true` when the fileset has a bibliography included, `false` otherwise.
 */
fun PsiFile.hasBibliography() = this.commandsInFileSet().any { it.name == "\\bibliography" }

/**
 * Checks if the fileset for this file uses \printbibliography, in which case the user probably wants to use biber.
 *
 * @return `true` when the fileset has a bibliography included, `false` otherwise.
 */
fun PsiFile.usesBiber() = this.commandsInFileSet().any { it.name == "\\printbibliography" }

/**
 * Looks up the first parent of a given child that has the given class.
 *
 * @param child
 * The child from which to find the parent of.
 * @param parentClass
 * The type the parent has.
 * @return The first parent that has the given class, or `null` when the parent can't be
 * found.
 */
fun <T : PsiElement?> getParentOfType(
    child: PsiElement?,
    parentClass: Class<T>
): T? {
    var element = child
    while (element != null) {
        if (parentClass.isAssignableFrom(element.javaClass)) {
            @Suppress("UNCHECKED_CAST")
            return element as T
        }
        element = element.parent
    }
    return element
}

val LatexParameterText.command: PsiElement?
    get() {
        return this.firstParentOfType(LatexCommands::class)?.firstChild
    }

// fun LatexOptionalKeyValPair.getKeyValValue() {
//    PsiTreeUtil.getChildOfType(this, LatexKeyValValue::class.java)
// }

package nl.hannahsten.texifyidea.util.parser

import com.intellij.openapi.application.runReadAction
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.PsiElementPattern
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.nextLeaf
import com.intellij.util.ProcessingContext
import nl.hannahsten.texifyidea.grammar.BibtexLanguage
import nl.hannahsten.texifyidea.grammar.LatexLanguage
import nl.hannahsten.texifyidea.lang.DefaultEnvironment
import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.lang.magic.TextBasedMagicCommentParser
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import kotlin.reflect.KClass

/**
 * Get the offset where the psi element ends.
 */
fun PsiElement.endOffset(): Int = textOffset + textLength

/**
 * @see [PsiTreeUtil.getChildrenOfType]
 */
fun <T : PsiElement> PsiElement.childrenOfType(clazz: KClass<T>): Collection<T> {
    if (project.isDisposed || !this.isValid) return emptyList()
    return runReadAction { PsiTreeUtil.findChildrenOfType(this, clazz.java) }
}

/**
 * @see [PsiTreeUtil.getChildrenOfType]
 */
inline fun <reified T : PsiElement> PsiElement.childrenOfType(): Collection<T> = childrenOfType(T::class)

/**
 * Finds the first element that matches a given predicate.
 */
@Suppress("UNCHECKED_CAST")
fun <T : PsiElement> PsiElement.findFirstChild(predicate: (PsiElement) -> Boolean): T? {
    for (child in children) {
        if (predicate(this)) {
            return this as? T
        }

        val first = child.findFirstChild<T>(predicate)
        if (first != null) {
            return first
        }
    }

    return null
}

/**
 * Finds the first child of a certain type.
 */
@Suppress("UNCHECKED_CAST")
fun <T : PsiElement> PsiElement.firstChildOfType(clazz: KClass<T>): T? {
    val children = runReadAction { this.children }
    for (child in children) {
        if (clazz.java.isAssignableFrom(child.javaClass)) {
            return child as? T
        }

        val first = child.firstChildOfType(clazz)
        if (first != null) {
            return first
        }
    }

    return null
}

/**
 * Finds the first parent of a certain type.
 */
@Suppress("UNCHECKED_CAST")
fun <T : PsiElement> PsiElement.firstParentOfType(clazz: KClass<T>): T? {
    var current: PsiElement? = this
    while (current != null) {
        if (clazz.java.isAssignableFrom(current.javaClass)) {
            return current as? T
        }
        current = current.parent
    }
    return null
}

/**
 * Finds the last child of a certain type.
 */
@Suppress("UNCHECKED_CAST")
fun <T : PsiElement> PsiElement.lastChildOfType(clazz: KClass<T>): T? {
    for (child in children.reversedArray()) {
        if (clazz.java.isAssignableFrom(child.javaClass)) {
            return child as? T
        }

        val last = child.lastChildOfType(clazz)
        if (last != null) {
            return last
        }
    }

    return null
}

/**
 * @see [PsiTreeUtil.getParentOfType]
 */
fun <T : PsiElement> PsiElement.parentOfType(clazz: KClass<T>): T? = PsiTreeUtil.getParentOfType(this, clazz.java)

/**
 * Checks if the psi element has a parent of a given class.
 */
fun <T : PsiElement> PsiElement.hasParent(clazz: KClass<T>): Boolean = parentOfType(clazz) != null

/**
 * Checks if the psi element is in math mode or not.
 *
 * @return `true` when the element is in math mode, `false` when the element is in no math mode.
 */
fun PsiElement.inMathContext(): Boolean {
    // Do the cheap tests first.
    return inDirectMathContext()
        // Check if any of the parents are in math context, because the direct environment might not be explicitly
        // defined as math context.
        || parents().any { it.inDirectMathContext() }
}

/**
 * Checks if the psi element is in a direct math context or not.
 */
fun PsiElement.inDirectMathContext(): Boolean =
    hasParent(LatexMathContent::class)
        || hasParent(LatexDisplayMath::class)
        || inDirectEnvironmentContext(Environment.Context.MATH)

/**
 * Returns the outer math environment.
 */
fun PsiElement?.findOuterMathEnvironment(): PsiElement? {
    var element = this
    var outerMathEnvironment: PsiElement? = null

    while (element != null) {
        // get to parent which is *IN* math content
        while (element != null && element.inMathContext().not()) {
            element = element.parent
        }
        // find the marginal element which is NOT IN math content
        while (element != null && element.inMathContext()) {
            element = element.parent
        }

        if (element != null) {
            outerMathEnvironment = when (element.parent) {
                is LatexInlineMath -> element.parent
                is LatexDisplayMath -> element.parent
                else -> element
            }
            element = element.parent
        }
    }
    return outerMathEnvironment
}

/**
 * Check if the element is in a comment or not.
 */
fun PsiElement.inComment() = inDirectEnvironmentContext(Environment.Context.COMMENT) || when (this) {
    is PsiComment -> true
    else -> this is LeafPsiElement && elementType == LatexTypes.COMMAND_TOKEN
}

/**
 * Checks if the element is inside a verbatim context.
 */
fun PsiElement.inVerbatim() = inDirectEnvironment(EnvironmentMagic.verbatim)

/**
 * Finds the previous sibling of an element but skips over whitespace.
 *
 * @receiver The element to get the previous sibling of.
 * @return The previous sibling of the given psi element, or `null` when there is no
 * previous sibling.
 */
fun PsiElement.previousSiblingIgnoreWhitespace(): PsiElement? {
    var sibling: PsiElement? = this
    while (sibling?.prevSibling.also { sibling = it } != null) {
        if (sibling !is PsiWhiteSpace) {
            return sibling
        }
    }
    return null
}

/**
 * Finds the next sibling of an element but skips over whitespace.
 *
 * @receiver The element to get the next sibling of.
 * @return The next sibling of the given psi element, or `null` when there is no previous
 * sibling.
 */
fun PsiElement.nextSiblingIgnoreWhitespace(): PsiElement? {
    var sibling: PsiElement? = this
    while (sibling?.nextSibling.also { sibling = it } != null) {
        if (sibling !is PsiWhiteSpace) {
            return sibling
        }
    }
    return null
}

/**
 * Finds the next leaf element (which is not necessarily a sibling) but skips over whitespace.
 *
 * @receiver The element to get the next sibling of.
 * @return The next sibling of the given psi element, or `null` when there is no previous
 * sibling.
 */
fun PsiElement.nextLeafIgnoreWhitespace(): PsiElement? {
    var leaf: PsiElement? = this
    while (leaf?.nextLeaf(true).also { leaf = it } != null) {
        if (leaf !is PsiWhiteSpace) {
            return leaf
        }
    }
    return null
}

/**
 * Finds the next sibling of the element that has the given type.
 * If the element has the given type, it is returned directly.
 *
 * @return The first following sibling of the given type, or `null` when the sibling couldn't be found.
 */
fun <T : PsiElement> PsiElement.nextSiblingOfType(clazz: KClass<T>): T? {
    return siblingOfType(clazz, PsiElement::getNextSibling)
}

/**
 * Finds the previous sibling of the element that has the given type.
 *
 * @return The first previous sibling of the given type, or `null` when the sibling couldn't be found.
 */
fun <T : PsiElement> PsiElement.previousSiblingOfType(clazz: KClass<T>): T? {
    return siblingOfType(clazz, PsiElement::getPrevSibling)
}

private fun <T : PsiElement> PsiElement.siblingOfType(clazz: KClass<T>, next: PsiElement.() -> PsiElement): T? {
    var sibling: PsiElement? = this
    while (sibling != null) {
        if (clazz.java.isAssignableFrom(sibling::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return sibling as T
        }

        sibling = sibling.next()
    }

    return null
}

/**
 * Finds the `generations`th parent of the psi element.
 */
fun PsiElement.grandparent(generations: Int): PsiElement? {
    var parent: PsiElement = this
    for (i in 1..generations) {
        parent = parent.parent ?: return null
    }
    return parent
}

/**
 * Checks if the psi element has a (grand) parent that matches the given predicate.
 */
inline fun PsiElement.hasParentMatching(maxDepth: Int, predicate: (PsiElement) -> Boolean): Boolean {
    var count = 0
    var parent = this.parent
    while (parent != null && parent !is PsiFile) {
        if (predicate(parent)) {
            return true
        }

        parent = parent.parent

        if (count++ > maxDepth) {
            return false
        }
    }

    return false
}

/**
 * Checks whether the psi element is part of a comment or not.
 */
fun PsiElement.isComment(): Boolean {
    return this is PsiComment || inDirectEnvironmentContext(Environment.Context.COMMENT)
}

fun PsiElement.isLatexOrBibtex() = language == LatexLanguage || language == BibtexLanguage

/**
 * Checks if the element is in a direct environment.
 *
 * This method does not take nested environments into account. Meaning that only the first parent environment counts.
 */
fun PsiElement.inDirectEnvironment(environmentName: String): Boolean = inDirectEnvironment(listOf(environmentName))

/**
 * Checks if the element is one of certain direct environments.
 *
 * This method does not take nested environments into account. Meaning that only the first parent environment counts.
 */
fun PsiElement.inDirectEnvironment(validNames: Collection<String>): Boolean {
    val environment = parentOfType(LatexEnvironment::class) ?: return false
    val nameText = environment.name() ?: return false
    return nameText.text in validNames
}

/**
 * Runs the given predicate on the direct environment element of this psi element.
 *
 * @return `true` when the predicate tests `true`, or `false` when there is no direct environment or when the
 *              predicate failed.
 */
inline fun PsiElement.inDirectEnvironmentMatching(predicate: (LatexEnvironment) -> Boolean): Boolean {
    val environment = parentOfType(LatexEnvironment::class) ?: return false
    return predicate(environment)
}

/**
 * Checks if the psi element is a child of `parent`.
 *
 * @return `true` when the element is a child of `parent`, `false` when the element is not a child of `parent` or when
 *          `parent` is `null`
 */
fun PsiElement.isChildOf(parent: PsiElement?): Boolean {
    if (parent == null) {
        return false
    }

    return hasParentMatching(1000) { it == parent }
}

/**
 * Checks if the psi element has a direct environment with the given context.
 */
fun PsiElement.inDirectEnvironmentContext(context: Environment.Context): Boolean {
    val environment = parentOfType(LatexEnvironment::class) ?: return context == Environment.Context.NORMAL
    return inDirectEnvironmentMatching {
        DefaultEnvironment.fromPsi(environment)?.context == context
    }
}

/**
 * Performs the given [action] on each child, in order.
 */
inline fun PsiElement.forEachChild(action: (PsiElement) -> Unit) {
    for (child in children) action(child)
}

/**
 * Performs the given [action] on each child of the given type `Psi`, in order.
 */
inline fun <reified Psi : PsiElement> PsiElement.forEachChildOfType(action: (PsiElement) -> Unit) = forEachChild {
    if (it is Psi) {
        action(it)
    }
}

/**
 * Finds the `n`th (index) child of the given type.
 */
inline fun <reified ChildPsi : PsiElement> PsiElement.nthChildOfType(index: Int): ChildPsi? {
    var pointer = 0
    forEachChildOfType<ChildPsi> {
        if (pointer == index) {
            return it as ChildPsi
        }
        pointer++
    }
    return null
}

/**
 * Finds the first child of the PsiElement that is not whitespace.
 * When there is no first child that is not whitespace, this function returns `null`.
 */
fun PsiElement.firstChildIgnoringWhitespaceOrNull(): PsiElement? {
    var child: PsiElement? = firstChild ?: return null
    while (child is PsiWhiteSpace) {
        child = child.nextSiblingIgnoreWhitespace()
    }
    return child
}

/**
 * Checks if the PsiComment is actually a Magic Comment.
 *
 * @return `true` if it is a magic comment, `false` otherwise.
 */
fun PsiElement?.containsMagicComment(): Boolean =
    this?.text?.let { t -> TextBasedMagicCommentParser.COMMENT_PREFIX.containsMatchIn(t) } ?: false

/**
 * Remove a psi element from the psi tree.
 *
 * Also remove the white space in front of the psi element when [removeLeadingWhiteSpace] is true, so we don't end up
 * with ridiculous amounts of white space.
 */
fun PsiElement.remove(removeLeadingWhiteSpace: Boolean = true) {
    if (removeLeadingWhiteSpace) {
        previousSiblingOfType(PsiWhiteSpace::class)?.let {
            it.parent.node.removeChild(it.node)
        }
    }
    parent.node.removeChild(node)
}

/**
 * Get a sequence of all the parents of this PsiElement with the given type.
 */
inline fun <reified Psi : PsiElement> PsiElement.parentsOfType(): Sequence<Psi> = parentsOfType(Psi::class)

/**
 * Get a sequence of all parents of this PsiElement that are of the given type.
 */
fun <Psi : PsiElement> PsiElement.parentsOfType(klass: KClass<out Psi>): Sequence<Psi> {
    return parents().filterIsInstance(klass.java)
}

/**
 * Get a sequence of all parents of this element.
 */
fun PsiElement.parents(): Sequence<PsiElement> = generateSequence(this) {
    it.parent?.run { if (!isValid) null else this }
}

/**
 * Adds the pattern condition to this psi element pattern.
 */
fun <Psi : PsiElement> PsiElementPattern.Capture<Psi>.withPattern(
    debugName: String? = null,
    acceptFunction: (psiElement: PsiElement, context: ProcessingContext?) -> Boolean
) = with(object : PatternCondition<PsiElement>(debugName) {

    // This helper function allows for a simple lambda on  the call site.
    override fun accepts(psiElement: PsiElement, context: ProcessingContext?): Boolean {
        return acceptFunction(psiElement, context)
    }
})

/**
 * Looks for the index of this child element in the children of the parent that share the type with this child.
 * Zero indexed.
 *
 * Example:
 * PARENT
 * - CHILD A
 * - CHILD B
 * - BREAD A
 * - BREAD B
 * - CHILD C
 *
 * `CHILD C.indexOfChildByType(PARENT) = 2`.
 *
 * @receiver The parent of the children to get the index of.
 * @return The index of this element in the child list, ignoring children of a different type, or `null` when the parent
 *          could not be found, or when no child could be found that matches `this`.
 */
inline fun <reified PsiChild : PsiElement, reified PsiParent : PsiElement> PsiChild.indexOfChildByType(): Int? {
    val parentElement = parentOfType(PsiParent::class) ?: return null

    // Loop over all children to find this parameter.
    var currentIndex = 0
    parentElement.forEachChildOfType<PsiChild> {
        if (it == this) {
            return currentIndex
        }
        currentIndex++
    }

    // No child found.
    return null
}

package nl.hannahsten.texifyidea.util.text

import nl.hannahsten.texifyidea.lang.Described

/**
 * @author Hannah Schellekens
 */
enum class Ipsum(override val description: String, val generateInput: () -> String) : Described {

    TEXIFY_IDEA_IPSUM("TeXiFy IDEA Ipsum", "/nl/hannahsten/texifyidea/ipsum/texify.txt".classpathReader()),
    ;

    override fun toString() = description
}

private fun String.classpathReader(): () -> String = {
    Ipsum::class.java.getResource(this).readText()
}

package nl.hannahsten.texifyidea.util.text

import nl.hannahsten.texifyidea.util.capitalizeFirst
import java.util.regex.Pattern
import kotlin.random.Random

/**
 * Generates random filler/dummy text based on a template.
 *
 * The generator uses a template input file in the following format:
 *
 * ```
 * KEY1
 * value1
 * value2
 * value3
 * ..
 * valueN
 *
 * KEY2
 * value1
 * ..
 * valueN
 *
 * TEXT
 * sentence1
 * sentence2
 * ..
 * sentenceN
 *
 * ```
 * Keys used in sentences (as `$KEY1`) will be substituted by a random value.
 *
 * @author Hannah Schellekens
 */
open class TexifyIpsumGenerator(

    /**
     * minimum amount of paragraphs..maximum amount of paragraphs.
     * Definitive amount is randomly determined.
     */
    private val numberOfParagraphs: IntRange = 3..7,

    /**
     * minimum amount of sentences in a paragraph..maximum amount of sentences in a paragraph.
     * Definitive amount is randomly determined for each paragraph.
     */
    private val numberOfSentences: IntRange = 2..20,

    /**
     * The random object to use to generate randomness.
     */
    val random: Random = Random(Random.nextInt()),

    /**
     * What ipsum template to use.
     */
    val ipsum: Ipsum = Ipsum.TEXIFY_IDEA_IPSUM
) {

    /**
     * Maps each KEY to the list of options.
     * Only options with the TEXT key can contain $TEMPLATE keys.
     */
    private val template: Map<String, List<String>> by lazy {
        val input = ipsum.generateInput()
        // Whether the newlines are Windows or Unix style depends on which system the plugin was built, so catch both
        val parts = input.split(Pattern.compile("(\\r?\\n){2,}"))

        parts.associate { templatePart ->
            val lines = templatePart.lines()
            val key = lines.firstOrNull() ?: error("Invalid template format, no lines found")
            key to lines.drop(1)
        }
    }

    /**
     * All available template keys.
     */
    private val templateKeys: Set<String> by lazy {
        template.keys - setOf("TEXT")
    }

    /**
     * All available sentences in the template.
     */
    private val sentences: List<String> by lazy {
        template["TEXT"] ?: error("No TEXT key found.")
    }

    /**
     * Generates a list of paragraphs (list of sentences).
     */
    fun generate(): List<List<String>> {
        val paragraphCount = numberOfParagraphs.random(random)
        return (1..paragraphCount).map {
            generateParagraph()
        }
    }

    /**
     * Generates a random paragraph with a random size in range [numberOfSentences].
     */
    private fun generateParagraph(): List<String> {
        val sentenceCount = numberOfSentences.random(random)
        return (1..sentenceCount).map {
            sentences.random(random).applyTemplates().capitalizeFirst()
        }
    }

    /**
     * Substitutes all templates with random subtitutions.
     */
    private fun String.applyTemplates(): String {
        var result = this
        templateKeys.forEach { key ->
            val substitutions = template[key] ?: error("Key '$key' not present in template.")
            while (result.contains("$$key")) {
                result = result.replaceFirst("$$key", substitutions.random(random))
            }
        }
        return result
    }
}

package nl.hannahsten.texifyidea.action.library.mendeley

import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.either
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.jcef.JBCefBrowser
import nl.hannahsten.texifyidea.RemoteLibraryRequestFailure
import nl.hannahsten.texifyidea.action.library.AddLibraryAction
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.remotelibraries.RemoteBibLibraryFactory
import nl.hannahsten.texifyidea.remotelibraries.RemoteLibraryManager
import nl.hannahsten.texifyidea.remotelibraries.mendeley.MendeleyAuthenticator
import nl.hannahsten.texifyidea.remotelibraries.mendeley.MendeleyLibrary
import nl.hannahsten.texifyidea.ui.remotelibraries.AddLibDialogWrapper
import javax.swing.JComponent

class AddMendeleyAction : AddLibraryAction<MendeleyLibrary, AddMendeleyAction.AddMendeleyDialogWrapper>() {

    override suspend fun createLibrary(dialogWrapper: AddMendeleyDialogWrapper, project: Project): Either<RemoteLibraryRequestFailure, Pair<MendeleyLibrary, List<BibtexEntry>>?> = either {
        val library = RemoteBibLibraryFactory.create(MendeleyLibrary.NAME) as? MendeleyLibrary ?: return@either null
        val bibItems = library.getCollection().getOrElse { raise(it) }
        RemoteLibraryManager.getInstance().updateLibrary(library, bibItems)
        library to bibItems
    }

    override fun getDialog(project: Project): AddMendeleyDialogWrapper {
        return AddMendeleyDialogWrapper(project)
    }

    /**
     * Reset the authorization server when the library is added, to prepare for the next time we want to add a library.
     */
    override fun onFinish() {
        MendeleyAuthenticator.reset()
    }

    class AddMendeleyDialogWrapper(
        val project: Project,
    ) : AddLibDialogWrapper(MendeleyLibrary.NAME) {

        private val browser = JBCefBrowser(MendeleyAuthenticator.authorizationUrl)

        init {
            init()
        }

        override fun createCenterPanel(): JComponent {
            return browser.component
        }

        override fun doValidate(): ValidationInfo? {
            return if (MendeleyAuthenticator.isUserAuthenticationFinished) null
            else ValidationInfo("You are not yet logged in to Mendeley")
        }
    }
}

package nl.hannahsten.texifyidea.action.library.zotero

import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.either
import com.intellij.credentialStore.Credentials
import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.openapi.project.Project
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import nl.hannahsten.texifyidea.RemoteLibraryRequestFailure
import nl.hannahsten.texifyidea.action.library.AddLibraryAction
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.remotelibraries.RemoteBibLibraryFactory
import nl.hannahsten.texifyidea.remotelibraries.RemoteLibraryManager
import nl.hannahsten.texifyidea.remotelibraries.zotero.ZoteroLibrary
import nl.hannahsten.texifyidea.ui.remotelibraries.AddLibDialogWrapper
import nl.hannahsten.texifyidea.util.CredentialAttributes.Zotero
import javax.swing.JComponent

class AddZoteroAction : AddLibraryAction<ZoteroLibrary, AddZoteroAction.AddZoteroDialogWrapper>() {

    override fun getDialog(project: Project): AddZoteroDialogWrapper {
        return AddZoteroDialogWrapper(project)
    }

    override suspend fun createLibrary(dialogWrapper: AddZoteroDialogWrapper, project: Project): Either<RemoteLibraryRequestFailure, Pair<ZoteroLibrary, List<BibtexEntry>>?> = either {
        val library = RemoteBibLibraryFactory.create<ZoteroLibrary>(ZoteroLibrary.NAME) ?: return@either null
        val credentials = Credentials(dialogWrapper.userID, dialogWrapper.userApiKey)
        PasswordSafe.instance.set(Zotero.userAttributes, credentials)
        val bibItems = library.getCollection().getOrElse { raise(it) }
        RemoteLibraryManager.getInstance().updateLibrary(library, bibItems)
        library to bibItems
    }

    class AddZoteroDialogWrapper(val project: Project) : AddLibDialogWrapper(ZoteroLibrary.NAME) {

        var userID: String = ""

        var userApiKey: String = ""

        init {
            init()
        }

        override fun createCenterPanel(): JComponent {
            return panel {
                row("User ID:") {
                    textField().bindText({ userID }, { userID = it }).focused()
                    contextHelp("You can find your user ID in Zotero Settings > Feeds/API.")
                }
                row("User API key:") {
                    textField().bindText({ userApiKey }, { userApiKey = it })
                    contextHelp("Create a new API key in Zotero Settings > Feeds/API > Create new private key")
                }
            }
        }
    }
}

package nl.hannahsten.texifyidea.action.wizard.graphic

import nl.hannahsten.texifyidea.lang.graphic.CaptionLocation
import nl.hannahsten.texifyidea.lang.graphic.FigureLocation

/**
 * @author Hannah Schellekens
 */
data class InsertGraphicData(
    val filePath: String,
    val relativePath: Boolean,
    val options: String,
    val center: Boolean,
    val placeInFigure: Boolean,
    /** `null` when [placeInFigure] is `false`, not `null` otherwise */
    val captionLocation: CaptionLocation? = null,
    /** `null` when [placeInFigure] is `false`, not `null` otherwise */
    val caption: String? = null,
    /** `null` when [placeInFigure] is `false`, not `null` otherwise */
    val shortCaption: String? = null,
    /** `null` when [placeInFigure] is `false`, not `null` otherwise */
    val label: String? = null,
    /** `null` when [placeInFigure] is `false`, not `null` otherwise */
    val positions: List<FigureLocation>? = null
)

package nl.hannahsten.texifyidea.action.wizard.graphic

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.graphic.CaptionLocation
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.files.relativizePath
import nl.hannahsten.texifyidea.util.files.removeFileExtension
import java.io.File

/**
 * Action that shows a dialog with a graphic insertion wizard, and inserts the graphic as latex at the location of the
 * cursor.
 *
 * @author Hannah Schellekens
 */
class InsertGraphicWizardAction(private val initialFile: File? = null) : AnAction() {

    /**
     * Opens and handles the graphic insertion wizard.
     */
    fun executeAction(file: VirtualFile, project: Project) {
        val editor = project.currentTextEditor() ?: return
        val document = editor.editor.document

        // Get the indentation from the current line.
        val indent = document.lineIndentationByOffset(editor.editor.caretOffset())

        // Create the dialog.
        val dialogWrapper = InsertGraphicWizardDialogWrapper(initialFilePath = initialFile?.absolutePath ?: "")

        // If the user pressed OK, do stuff.
        if (!dialogWrapper.showAndGet()) return

        // Handle result.
        val graphicData = dialogWrapper.extractData()
        file.psiFile(project)?.let { graphicData.importPackages(it) }
        editor.editor.insertGraphic(project, graphicData, indent)
    }

    override fun actionPerformed(e: AnActionEvent) {
        val file = e.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return
        val project = e.getData(PlatformDataKeys.PROJECT) ?: return
        executeAction(file, project)
    }

    override fun update(e: AnActionEvent) {
        super.update(e)

        val file = e.getData(PlatformDataKeys.PSI_FILE)
        val shouldDisplayMenu = file?.isLatexFile() == true
        e.presentation.isVisible = shouldDisplayMenu
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT

    private fun Editor.insertGraphic(project: Project, data: InsertGraphicData, indent: String, tab: String = "    ") {
        // Only the graphics (non-centered).
        val toInsert = if (data.center.not() && data.placeInFigure.not()) {
            data.includeCommand(project)
        }
        // Centered graphics, but not in a figure.
        else if (data.center && data.placeInFigure.not()) {
            buildString {
                append("\\begin{center}\n")
                append(indent).append(tab).append(data.includeCommand(project)).newline()
                append(indent).append("\\end{center}")
            }
        }
        // Insert figure.
        else data.figure(project, indent, tab)

        insertAtCaretAndMove(toInsert)
    }

    private fun InsertGraphicData.figure(project: Project, indent: String, tab: String) = buildString {
        append("\\begin{figure}")
        if (positions?.isNotEmpty() == true) {
            append(positionOptions())
        }
        newline()

        if (center) {
            append(indent).append(tab).append("\\centering").newline()
        }

        if (captionLocation == CaptionLocation.ABOVE_GRAPHIC) {
            addCaptionAndLabel(this@figure, indent, tab)
        }

        append(indent).append(tab).append(includeCommand(project)).newline()

        if (captionLocation == CaptionLocation.BELOW_GRAPHIC) {
            addCaptionAndLabel(this@figure, indent, tab)
        }

        append(indent).append("\\end{figure}")
    }

    private fun StringBuilder.addCaptionAndLabel(data: InsertGraphicData, indent: String, tab: String) {
        append(indent).append(tab).append(data.captionCommand()).newline()
        append(indent).append(tab).append("\\label{").append(data.label ?: "").append("}").newline()
    }

    private fun InsertGraphicData.includeCommand(project: Project) = buildString {
        append("\\includegraphics")
        if (options.isNotBlank()) {
            append("[").append(options).append("]")
        }
        append("{").append(convertFilePath(project, filePath)).append("}")
    }

    private fun InsertGraphicData.convertFilePath(project: Project, absoluteFilePath: String): String {
        val rootManager = ProjectRootManager.getInstance(project)

        val filePath = if (relativePath) {
            rootManager.relativizePath(absoluteFilePath) ?: absoluteFilePath
        }
        else absoluteFilePath

        return filePath.removeFileExtension()
    }

    private fun InsertGraphicData.captionCommand() = buildString {
        append("\\caption")
        if (shortCaption?.isNotBlank() == true) {
            append("[").append(shortCaption).append("]")
        }
        append("{").append(caption ?: "").append("}")
    }

    private fun InsertGraphicData.importPackages(file: PsiFile) {
        WriteCommandAction.runWriteCommandAction(file.project) {
            file.insertUsepackage(LatexPackage.GRAPHICX)
            positions?.forEach { location ->
                location.requiredPackage?.let {
                    file.insertUsepackage(it)
                }
            }
        }
    }

    private fun InsertGraphicData.positionOptions() = buildString {
        append("[")
        append(positions?.joinToString("") { it.symbol })
        append("]")
    }
}

package nl.hannahsten.texifyidea.action.wizard.graphic

import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.TextBrowseFolderListener
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.ui.TitledSeparator
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import com.intellij.ui.components.fields.ExpandableTextField
import com.intellij.ui.components.panels.HorizontalLayout
import nl.hannahsten.texifyidea.lang.graphic.CaptionLocation
import nl.hannahsten.texifyidea.lang.graphic.FigureLocation
import nl.hannahsten.texifyidea.util.addKeyReleasedListener
import nl.hannahsten.texifyidea.util.addLabeledComponent
import nl.hannahsten.texifyidea.util.addTextChangeListener
import nl.hannahsten.texifyidea.util.magic.FileMagic
import nl.hannahsten.texifyidea.util.setInputFilter
import java.awt.Dimension
import java.util.*
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JPanel
import javax.swing.JTextField

/**
 * @author Hannah Schellekens
 */
open class InsertGraphicWizardDialogWrapper(val initialFilePath: String = "") : DialogWrapper(true) {

    /**
     * Stores the path to the graphics file.
     */
    private val txtGraphicFile = TextFieldWithBrowseButton().apply {
        text = initialFilePath
        addBrowseFolderListener(
            TextBrowseFolderListener(
                FileChooserDescriptor(true, false, false, false, false, false)
                    .withFileFilter { vf -> vf.extension?.lowercase(Locale.getDefault()) in FileMagic.graphicFileExtensions }
                    .withTitle("Select Graphics File...")
            )
        )
    }

    /**
     * Insert a relative file path if checked, absolute if unchecked.
     */
    private val checkRelativePath = JBCheckBox("Convert to relative path", true)

    /**
     * The width option for the graphic. Not necessarily a number. Whatever is in here will get put
     * in the optional parameters.
     */
    private val txtWidth = JBTextField("", 8).apply {
        addTextChangeListener {
            updateGraphicsOptions("width")
        }
    }

    /**
     * The height option for the graphic. Not necessarily a number. Whatever is in here will get put
     * in the optional parameters.
     */
    private val txtHeight = JBTextField("", 8).apply {
        addTextChangeListener {
            updateGraphicsOptions("height")
        }
    }

    /**
     * The angle option for the graphic. When empty, no angle. Not necessarily a number.
     */
    private val txtAngle = JBTextField("").apply {
        toolTipText = "Degrees, anticlockwise"
        addTextChangeListener {
            updateGraphicsOptions("angle")
        }
    }

    /**
     * The custom graphics options. This is basically the optional parameter of \includegraphics.
     * Can get modified when the width/height/angle get modified.
     */
    private val txtCustomOptions = JBTextField("")

    /**
     * Whether the image must be centered horizontally.
     */
    private val checkCenterHorizontally = JBCheckBox("Center horizontally", true)

    /**
     * Whether to place the included graphic inside a figure environment.
     */
    private val checkPlaceInFigure = JBCheckBox("Place in figure environment", true).apply {
        addItemListener {
            updateFigureControlsState()
        }
    }

    /**
     * Where to put the caption in the figure environment.
     */
    private val cboxCaptionLocation = ComboBox(CaptionLocation.values()).apply {
        selectedItem = CaptionLocation.BELOW_GRAPHIC
    }

    /**
     * Stores the short caption for the figure environment.
     */
    private val txtShortCaption = JBTextField()

    /**
     * Stores the long caption for the figure environment.
     */
    private val txtLongCaption = ExpandableTextField(
        { it.split("\n") },
        { it.joinToString("\n") }
    )

    /**
     * Contains the label for the figure.
     */
    private val txtLabel = JBTextField("fig:")

    /**
     * Contains the positioning symbols.
     */
    private val txtPosition = JBTextField("").apply {
        setInputFilter(FigureLocation.ALL_SYMBOLS.toSet())
        addKeyReleasedListener { updatePositionCheckmarks() }
    }

    /**
     * The check boxes for the figure locations.
     */
    private val checkPosition = FigureLocation.values().asSequence()
        .map { location ->
            location.symbol to JBCheckBox(location.description).apply {
                addActionListener { event ->
                    val source = event.source as? JBCheckBox ?: error("Not a JBCheckBox!")
                    // Add symbol if selected.
                    if (source.isSelected && txtPosition.text.contains(location.symbol).not()) {
                        txtPosition.text = txtPosition.text + location.symbol
                    }
                    // Remove if deselected.
                    else {
                        txtPosition.text = txtPosition.text.replace(location.symbol, "")
                    }
                }
            }
        }
        // Use linked hash map to preserve order.
        .toMap(LinkedHashMap())

    init {
        super.init()
        title = "Insert Graphic"
    }

    /**
     * Get the data that has been entered into the UI.
     */
    fun extractData() = InsertGraphicData(
        filePath = txtGraphicFile.text.trim(),
        relativePath = checkRelativePath.isSelected,
        options = txtCustomOptions.text.trim(),
        center = checkCenterHorizontally.isSelected,
        placeInFigure = checkPlaceInFigure.isSelected,
        captionLocation = whenFigure { cboxCaptionLocation.selectedItem as CaptionLocation },
        caption = whenFigure { txtLongCaption.text.trim() },
        shortCaption = whenFigure { txtShortCaption.text.trim() },
        label = whenFigure { txtLabel.text.trim() },
        positions = whenFigure {
            txtPosition.text.trim().mapNotNull { FigureLocation.bySymbol(it.toString()) }
        }
    )

    /**
     * Only selects the value when [checkPlaceInFigure] is selected.
     */
    private fun <T> whenFigure(value: () -> T): T? = if (checkPlaceInFigure.isSelected) value() else null

    override fun createCenterPanel() = JPanel().apply {
        layout = BoxLayout(this, BoxLayout.Y_AXIS).apply {
            // Flush left.
            alignmentX = 0.0f
        }

        addFileControls()
        addGraphicOptionControls()
        addLayoutControls()
        addFigureControls()

        add(Box.createRigidArea(Dimension(0, 12)))
    }

    private fun JPanel.addFileControls() {
        addSectionHeader("Graphics file")

        val labelWidth = 64
        addLabeledComponent(txtGraphicFile, "Path:", labelWidth)
        addLabeledComponent(checkRelativePath, "", labelWidth)
    }

    private fun JPanel.addGraphicOptionControls() {
        addSectionHeader("Graphic options")

        val labelWidth = 64
        val optionsPanel = JPanel(HorizontalLayout(0)).apply {
            addLabeledComponent(txtWidth, "Width:", labelWidth)
            addLabeledComponent(txtHeight, "Height:", labelWidth)
            addLabeledComponent(txtAngle, "Angle:", labelWidth)
        }
        add(optionsPanel)
        addLabeledComponent(txtCustomOptions, "Custom:", labelWidth)
    }

    private fun JPanel.addLayoutControls() {
        addSectionHeader("Layout options")

        addLabeledComponent(checkCenterHorizontally, "")
    }

    private fun JPanel.addFigureControls() {
        addSectionHeader("Figure environment")

        val labelWidth = 112
        addLabeledComponent(checkPlaceInFigure, "")
        addLabeledComponent(cboxCaptionLocation, "Caption location:", labelWidth)
        addLabeledComponent(txtLongCaption, "Caption:", labelWidth)
        addLabeledComponent(txtShortCaption, "Short caption:", labelWidth)
        addLabeledComponent(txtLabel, "Label:", labelWidth)
        addLabeledComponent(txtPosition, "Position:", labelWidth)

        val positionBoxes = JPanel(HorizontalLayout(8)).apply {
            checkPosition.values.forEach {
                add(it)
            }
        }
        addLabeledComponent(positionBoxes, "", labelWidth)
    }

    private fun JPanel.addSectionHeader(title: String, margin: Int = 8) {
        add(Box.createRigidArea(Dimension(0, margin)))
        add(TitledSeparator(title))
        add(Box.createRigidArea(Dimension(0, margin)))
    }

    private fun updateFigureControlsState() {
        val enabled = checkPlaceInFigure.isSelected

        cboxCaptionLocation.isEnabled = enabled
        txtLongCaption.isEnabled = enabled
        txtShortCaption.isEnabled = enabled
        txtLabel.isEnabled = enabled
        checkPosition.values.forEach {
            it.isEnabled = enabled
        }
    }

    private fun JTextField.updateGraphicsOptions(fieldName: String) {
        val text = text.replace(",", "")
        // Update
        if (txtCustomOptions.text.contains("$fieldName=")) {
            txtCustomOptions.text = txtCustomOptions.text
                .replace(Regex("$fieldName=[^,]*"), Regex.escapeReplacement("$fieldName=$text"))
        }
        // Nothing yet, set width property.
        else if (txtCustomOptions.text.isBlank()) {
            txtCustomOptions.text = "$fieldName=$text"
        }
        // When there is something, append width property.
        else {
            txtCustomOptions.text = txtCustomOptions.text + ",$fieldName=$text"
        }
    }

    private fun updatePositionCheckmarks() {
        val positionText = txtPosition.text
        checkPosition.forEach { (symbol, checkbox) ->
            checkbox.isSelected = symbol in positionText
        }
    }
}

package nl.hannahsten.texifyidea.action.wizard.ipsum

import nl.hannahsten.texifyidea.lang.Described
import nl.hannahsten.texifyidea.util.text.Ipsum
import java.util.*
import kotlin.random.Random

/**
 * @author Hannah Schellekens
 */
data class DummyTextData(
    val ipsumType: IpsumType,

    val blindtextType: BlindtextType = BlindtextType.PARAGRAPH,
    val blindtextRepetitions: Int = 1,
    val blindtextParagraphs: Int = 1,
    val blindtextLevel: Int = 1,

    val lipsumParagraphs: IntRange = 1..7,
    val lipsumSentences: IntRange = 1..999,
    val lipsumParagraphSeparator: LipsumParagraphSeparation = LipsumParagraphSeparation.PARAGRAPH,

    val rawDummyTextType: Ipsum = Ipsum.TEXIFY_IDEA_IPSUM,
    val rawParagraphs: IntRange = 3..7,
    val rawSentencesPerParagraph: IntRange = 2..20,
    val rawSeed: Int = Random.nextInt()
) {

    /**
     * @author Hannah Schellekens
     */
    enum class IpsumType(override val description: String) : Described {

        BLINDTEXT("blindtext package"),
        LIPSUM("lipsum package"),
        RAW("raw text");

        override fun toString() = description
    }

    /**
     * @author Hannah Schellekens
     */
    enum class BlindtextType(val commandNoSlash: String) {

        DOCUMENT("blinddocument"),
        PARAGRAPH("blindtext"),
        ITEMIZE("blindlist{itemize}"),
        ENUMERATE("blindlist{enumerate}"),
        DESCRIPTION("blindlist{description}");

        override fun toString() = name.lowercase(Locale.getDefault())
    }

    /**
     * @author Hannah Schellekens
     */
    enum class LipsumParagraphSeparation {

        PARAGRAPH,
        SPACE;

        override fun toString() = name.lowercase(Locale.getDefault())
    }
}

package nl.hannahsten.texifyidea.action.wizard.ipsum

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.text.TexifyIpsumGenerator
import kotlin.random.Random

/**
 * @author Hannah Schellekens
 */
open class InsertDummyTextAction : AnAction() {

    /**
     * Opens and handles the dummy text UI.
     */
    private fun executeAction(file: PsiFile) {
        val project = file.project
        val editor = project.currentTextEditor()?.editor ?: return

        // Get the indentation from the current line.
        val indent = editor.document.lineIndentationByOffset(editor.caretOffset())

        // Create the dialog.
        val dialog = InsertDummyTextDialogWrapper()

        // If the user pressed OK, do stuff.
        if (dialog.showAndGet().not()) return
        val data = dialog.extractData()
        editor.insertDummyText(file, data, indent)
    }

    override fun actionPerformed(e: AnActionEvent) {
        val file = e.getData(PlatformDataKeys.PSI_FILE) ?: return
        executeAction(file)
    }

    override fun update(e: AnActionEvent) {
        super.update(e)

        val file = e.getData(PlatformDataKeys.PSI_FILE)
        val shouldDisplayMenu = file?.isLatexFile() == true
        e.presentation.isVisible = shouldDisplayMenu
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT

    private fun Editor.insertDummyText(file: PsiFile, data: DummyTextData, indent: String) = when (data.ipsumType) {
        DummyTextData.IpsumType.BLINDTEXT -> insertBlindtext(file, data)
        DummyTextData.IpsumType.LIPSUM -> insertLipsum(file, data)
        DummyTextData.IpsumType.RAW -> insertRaw(data, indent)
    }

    private fun Editor.insertBlindtext(file: PsiFile, data: DummyTextData) {
        // Import blindtext
        WriteCommandAction.runWriteCommandAction(file.project) {
            file.insertUsepackage(LatexPackage.BLINDTEXT)
        }

        // When itemize/enumerate/description is selected the level can be selected as well when larger than 1.
        val type = data.blindtextType
        if ((
            type == DummyTextData.BlindtextType.ITEMIZE || type == DummyTextData.BlindtextType.ENUMERATE ||
                type == DummyTextData.BlindtextType.DESCRIPTION
            ) && data.blindtextLevel > 1
        ) {
            val command = "\\" + type.commandNoSlash.replace("list", "listlist[${data.blindtextLevel}]")
            insertAtCaretAndMove(command)
            return
        }

        // When a paragraph only has repetitions (so no paragraphs), use the base \blindtext[n] command.
        if (type == DummyTextData.BlindtextType.PARAGRAPH && data.blindtextParagraphs == 1 && data.blindtextRepetitions > 1) {
            insertAtCaretAndMove("\\blindtext[${data.blindtextRepetitions}]")
            return
        }

        // When a pragraph has also paragraphs, use the \Blindtext[paragraphs][repetitions] version,
        if (type == DummyTextData.BlindtextType.PARAGRAPH && data.blindtextParagraphs > 1) {
            insertAtCaretAndMove("\\Blindtext[${data.blindtextParagraphs}][${data.blindtextRepetitions}]")
            return
        }

        // Otherwise, there is no special treatment needed, so just insert the command.
        val baseCommand = data.blindtextType.commandNoSlash
        insertAtCaretAndMove("\\$baseCommand")
    }

    private fun Editor.insertLipsum(file: PsiFile, data: DummyTextData) {
        // Import blindtext
        WriteCommandAction.runWriteCommandAction(file.project) {
            file.insertUsepackage(LatexPackage.LIPSUM)
        }

        val star = if (data.lipsumParagraphSeparator == DummyTextData.LipsumParagraphSeparation.SPACE) "*" else ""

        // By default, \lipsum prints the first 7 paragraphs.
        if (data.lipsumParagraphs == 1..7 && data.lipsumSentences == 1..999) {
            insertAtCaretAndMove("\\lipsum$star")
            return
        }

        // When the sentences is at max value (999), assume no specific sentences are selected => only first optional
        // parameter.
        val paragraphRange = data.lipsumParagraphs.toRangeString()
        if (data.lipsumSentences == 1..999) {
            insertAtCaretAndMove("\\lipsum$star[$paragraphRange]")
            return
        }

        // Otherwise, specify both paragraphs and sentences.
        val sentenceRange = data.lipsumSentences.toRangeString()
        insertAtCaretAndMove("\\lipsum$star[$paragraphRange][$sentenceRange]")
    }

    private fun Editor.insertRaw(data: DummyTextData, indent: String) {
        val generator = TexifyIpsumGenerator(
            data.rawParagraphs, data.rawSentencesPerParagraph, Random(data.rawSeed), data.rawDummyTextType
        )
        val dummyText = generator.generate()

        val result = StringBuilder()
        var currentIndent = ""
        dummyText.forEach { paragraph ->
            paragraph.forEach { sentence ->
                result.append(currentIndent).append(sentence).append("\n")
                currentIndent = indent
            }
            result.append("\n")
        }

        insertAtCaretAndMove(result.toString().trimEnd())
    }
}

package nl.hannahsten.texifyidea.action.wizard.ipsum

import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.JBIntSpinner
import com.intellij.ui.components.JBTabbedPane
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.JBUI
import nl.hannahsten.texifyidea.util.addLabeledComponent
import nl.hannahsten.texifyidea.util.hbox
import nl.hannahsten.texifyidea.util.text.Ipsum
import java.awt.BorderLayout
import java.awt.Dimension
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JLabel
import javax.swing.JPanel
import kotlin.random.Random

/**
 * @author Hannah Schellekens
 */
open class InsertDummyTextDialogWrapper : DialogWrapper(true) {

    /**
     * What type of blind text to generate.
     */
    private val cboxBlindType = ComboBox(DummyTextData.BlindtextType.values()).apply {
        selectedItem = DummyTextData.BlindtextType.PARAGRAPH
        addItemListener { updateUi() }
    }

    /**
     * The amount of repetitions of blind text.
     */
    private val intBlindRepetitions = JBIntSpinner(1, 1, 99999)

    /**
     * The amount of paragraphs of blind text.
     */
    private val intBlindParagraphs = JBIntSpinner(1, 1, 99999)

    /**
     * The level of the blind itemize/enumerate/description.
     */
    private val intBlindLevel = JBIntSpinner(1, 1, 999)

    /**
     * Panel containing options for the blindtext package.
     */
    private val panelBlindtext = JPanel().apply {
        border = JBUI.Borders.empty(8, 0, 8, 16)
        layout = BoxLayout(this, BoxLayout.Y_AXIS).apply {
            // Flush left.
            alignmentX = 0.0f
        }

        val labelWidth = 140
        addLabeledComponent(cboxBlindType, "Type of text:", labelWidth)
        addLabeledComponent(hbox(8, intBlindParagraphs, JLabel("Repetitions:"), intBlindRepetitions), "Paragraphs:", labelWidth)
        addLabeledComponent(intBlindLevel, "List level:", labelWidth)
        add(Box.createRigidArea(Dimension(0, 28)))
    }

    /**
     * Contains the lipsum start paragraph number (1-150).
     */
    private val intLipsumParagraphsMin = JBIntSpinner(1, 1, 150)

    /**
     * Contains the lipsum end paragraph number (1-150).
     */
    private val intLipsumParagraphsMax = JBIntSpinner(7, 1, 150)

    /**
     * Contains the lipsum start sentence number.
     */
    private val intLipsumSentencesMin = JBIntSpinner(1, 1, 999)

    /**
     * Contains the lipsum end sentence number.
     */
    private val intLipsumSentencesMax = JBIntSpinner(999, 1, 999)

    /**
     * How to separate the lipsum paragraphs.
     */
    private val cboxLipsumSeparator = ComboBox(DummyTextData.LipsumParagraphSeparation.values()).apply {
        selectedItem = DummyTextData.LipsumParagraphSeparation.PARAGRAPH
    }

    /**
     * Panel containing options for the lipsum package.
     */
    private val panelLipsum = JPanel().apply {
        border = JBUI.Borders.empty(8, 0, 8, 16)
        layout = BoxLayout(this, BoxLayout.Y_AXIS).apply {
            // Flush left.
            alignmentX = 0.0f
        }

        val labelWidth = 192
        addLabeledComponent(hbox(8, intLipsumParagraphsMin, JLabel("to"), intLipsumParagraphsMax), "Paragraph numbers:", labelWidth)
        addLabeledComponent(hbox(8, intLipsumSentencesMin, JLabel("to"), intLipsumSentencesMax), "Sentence numbers:", labelWidth)
        addLabeledComponent(cboxLipsumSeparator, "Paragraph separation:", labelWidth)
        add(Box.createRigidArea(Dimension(0, 28)))
    }

    /**
     * Which dummy text teplate to use.
     */
    private val cboxRawTemplate = ComboBox(Ipsum.values()).apply {
        selectedItem = Ipsum.TEXIFY_IDEA_IPSUM
    }

    /**
     * The minimum number of raw paragraphs to generate.
     */
    private val intRawParagraphsMin = JBIntSpinner(3, 1, 99999)

    /**
     * The maximum number of raw paragraphs to generate.
     */
    private val intRawParagraphsMax = JBIntSpinner(7, 1, 99999)

    /**
     * The minimum number of raw sentences in a paragraph.
     */
    private val intRawSentencesMin = JBIntSpinner(2, 1, 99999)

    /**
     * The maximum number of raw sentences in a paragraph.
     */
    private val intRawSentencessMax = JBIntSpinner(14, 1, 99999)

    /**
     * Contains the seed to generate random numbers.
     */
    private val txtRawSeed = JBTextField(Random.nextInt().toString())

    /**
     * Panel containing options for raw text.
     */
    private val panelRaw = JPanel().apply {
        border = JBUI.Borders.empty(8, 0, 8, 16)
        layout = BoxLayout(this, BoxLayout.Y_AXIS).apply {
            // Flush left.
            alignmentX = 0.0f
        }

        val labelWidth = 192
        addLabeledComponent(cboxRawTemplate, "Dummy text template:", labelWidth)
        addLabeledComponent(hbox(8, intRawParagraphsMin, JLabel("to"), intRawParagraphsMax), "Number of paragraphs:", labelWidth)
        addLabeledComponent(hbox(8, intRawSentencesMin, JLabel("to"), intRawSentencessMax), "Sentences per paragraph:", labelWidth)
        addLabeledComponent(txtRawSeed, "Seed:", labelWidth)
    }

    private val tabPane = JBTabbedPane().apply {
        insertTab(DummyTextData.IpsumType.BLINDTEXT.description, null, panelBlindtext, null, 0)
        insertTab(DummyTextData.IpsumType.LIPSUM.description, null, panelLipsum, null, 1)
        insertTab(DummyTextData.IpsumType.RAW.description, null, panelRaw, null, 2)
    }

    init {
        super.init()
        title = "Insert Dummy Text"
    }

    /**
     * Get the data that has been entered into the UI.
     */
    fun extractData() = when (tabPane.selectedIndex) {
        0 -> DummyTextData(
            ipsumType = DummyTextData.IpsumType.BLINDTEXT,
            blindtextType = cboxBlindType.selectedItem as DummyTextData.BlindtextType,
            blindtextRepetitions = intBlindRepetitions.number,
            blindtextParagraphs = intBlindParagraphs.number,
            blindtextLevel = intBlindLevel.number
        )
        1 -> DummyTextData(
            ipsumType = DummyTextData.IpsumType.LIPSUM,
            lipsumParagraphs = intLipsumParagraphsMin.number..intLipsumParagraphsMax.number,
            lipsumSentences = intLipsumSentencesMin.number..intLipsumSentencesMax.number,
            lipsumParagraphSeparator = cboxLipsumSeparator.selectedItem as DummyTextData.LipsumParagraphSeparation
        )
        else -> DummyTextData(
            ipsumType = DummyTextData.IpsumType.RAW,
            rawDummyTextType = cboxRawTemplate.selectedItem as Ipsum,
            rawParagraphs = intRawParagraphsMin.number..intRawParagraphsMax.number,
            rawSentencesPerParagraph = intRawSentencesMin.number..intRawSentencessMax.number,
            rawSeed = txtRawSeed.text.toInt()
        )
    }

    private fun updateUi() {
        intBlindLevel.isEnabled = cboxBlindType.item == DummyTextData.BlindtextType.ITEMIZE ||
            cboxBlindType.item == DummyTextData.BlindtextType.DESCRIPTION ||
            cboxBlindType.item == DummyTextData.BlindtextType.ENUMERATE

        intBlindRepetitions.isEnabled = cboxBlindType.item == DummyTextData.BlindtextType.PARAGRAPH
        intBlindParagraphs.isEnabled = cboxBlindType.item == DummyTextData.BlindtextType.PARAGRAPH
    }

    override fun createCenterPanel() = JPanel(BorderLayout()).apply {
        add(tabPane, BorderLayout.CENTER)
        updateUi()
    }

    override fun doValidate() = if (intLipsumParagraphsMax.number < intLipsumParagraphsMin.number) {
        ValidationInfo("Maximum must be greater than or equal to the minimum.", intLipsumParagraphsMax)
    }
    else if (intLipsumSentencesMax.number < intLipsumSentencesMin.number) {
        ValidationInfo("Maximum must be greater than or equal to the minimum.", intLipsumSentencesMax)
    }
    else if (intRawParagraphsMax.number < intRawParagraphsMin.number) {
        ValidationInfo("Maximum must be greater than or equal to the minimum.", intRawParagraphsMax)
    }
    else if (intRawSentencessMax.number < intRawSentencesMin.number) {
        ValidationInfo("Maximum must be greater than or equal to the minimum.", intRawSentencessMax)
    }
    else if (txtRawSeed.text.toIntOrNull() == null) {
        ValidationInfo("Invalid seed: must be an integer in range -2147483648 to 2147483647.", txtRawSeed)
    }
    else null
}

package nl.hannahsten.texifyidea.action.wizard.table

/**
 * @author Abby Berkers
 */
enum class ColumnType(val displayName: String) {

    TEXT_COLUMN("Text"),
    MATH_COLUMN("Math"),
    NUMBERS_COLUMN("Numbers")
}

package nl.hannahsten.texifyidea.action.wizard.table

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.action.insert.InsertTable
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.caretOffset
import nl.hannahsten.texifyidea.util.currentTextEditor
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.insertUsepackage
import nl.hannahsten.texifyidea.util.lineIndentationByOffset
import java.util.*

/**
 * Action that shows a dialog with a table creation wizard, and inserts the table as latex at the location of the
 * cursor when clicking OK.
 *
 * @author Abby Berkers
 */
class LatexTableWizardAction : AnAction() {

    fun executeAction(file: VirtualFile, project: Project, defaultDialogWrapper: TableCreationDialogWrapper? = null) {
        val editor = project.currentTextEditor() ?: return
        val document = editor.editor.document

        // Get the indentation from the current line.
        val indent = document.lineIndentationByOffset(editor.editor.caretOffset())

        // Create the dialog.
        val dialogWrapper = defaultDialogWrapper ?: TableCreationDialogWrapper()

        // If the user pressed OK, do stuff.
        if (dialogWrapper.showAndGet()) {
            // Get the table information from the dialog, and convert it to latex.
            with(dialogWrapper.tableInformation) {
                val tableTextToInsert = convertTableToLatex(indent)

                // Use an insert action to insert the table.
                InsertTable(tableTextToInsert).actionPerformed(file, project, editor)
            }

            // Insert the booktabs package.
            WriteCommandAction.runWriteCommandAction(
                project,
                "Insert Table",
                "LaTeX",
                { file.psiFile(project)?.insertUsepackage(LatexPackage.BOOKTABS) },
                file.psiFile(project)
            )
        }
    }

    override fun actionPerformed(e: AnActionEvent) {
        val file = e.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return
        val project = e.getData(PlatformDataKeys.PROJECT) ?: return
        executeAction(file, project)
    }

    override fun update(e: AnActionEvent) {
        super.update(e)

        val file = e.getData(PlatformDataKeys.PSI_FILE)
        val shouldDisplayMenu = file?.isLatexFile() == true
        e.presentation.isVisible = shouldDisplayMenu
    }

    override fun getActionUpdateThread() = ActionUpdateThread.BGT

    /**
     * Function to process the contents of the table, i.e. the header text and table entries. The indentation
     * is the same throughout the table contents.
     */
    private fun TableInformation.processTableContent(indent: String): String {
        val headers = tableModel.getColumnNames()
            .joinToString(
                prefix = "$indent\\toprule\n$indent",
                separator = " & ",
                postfix = " \\\\\n$indent\\midrule\n"
            ) { "\\textbf{$it}" }

        val rows = tableModel.dataVector.joinToString(separator = "\n", postfix = "\n$indent\\bottomrule\n") { row ->
            (row as Vector<*>).joinToString(
                prefix = indent,
                separator = " & ",
                postfix = " \\\\"
            ) {
                // Enclose with $ if the type of this column is math.
                val index = row.indexOf(it)
                val encloseWith = if (columnTypes[index] == ColumnType.MATH_COLUMN) "$" else ""
                encloseWith + it.toString() + encloseWith
            }
        }
        return headers + rows
    }

    /**
     * Convert the table information to a latex table that can be inserted into the file.
     *
     * @param lineIndent
     *          The indentation of the current line, to be used on each new line.
     * @param tabIndent
     *          The continuation indent.
     */
    private fun TableInformation.convertTableToLatex(lineIndent: String, tabIndent: String = "    "): String {
        // The tex(t) for a table consists of three parts: the open commands, the actual content, and the closing commands
        // (this includes the caption and label).
        val openTableCommand = "\\begin{table}\n" +
            "$lineIndent$tabIndent\\centering\n" +
            // Everything within the table command gets an extra indent.
            "$lineIndent$tabIndent\\begin{tabular}{${columnTypes.toLatexColumnFormatters()}}\n"

        // The content has to be indented once more.
        val content = processTableContent(indent = lineIndent + tabIndent + tabIndent)

        val closeTableCommand = "$lineIndent$tabIndent\\end{tabular}\n" +
            "$lineIndent$tabIndent\\caption{$caption}\n" +
            "$lineIndent$tabIndent\\label{$label}\n" +
            "$lineIndent\\end{table}\n" +
            lineIndent // Indentation on the last line so we can continue typing there.

        return openTableCommand + content + closeTableCommand
    }

    /**
     * Convert the list of column types to a latex column format.
     */
    private fun List<ColumnType>.toLatexColumnFormatters(): String = joinToString(separator = "") {
        when (it) {
            ColumnType.TEXT_COLUMN -> "l"
            ColumnType.MATH_COLUMN -> "l"
            ColumnType.NUMBERS_COLUMN -> "r"
        }
    }
}

package nl.hannahsten.texifyidea.action.wizard.table

import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.AnActionButton
import com.intellij.ui.JBColor
import com.intellij.ui.LayeredIcon
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.ui.scale.JBUIScale
import com.intellij.ui.table.JBTable
import com.intellij.util.IconUtil
import nl.hannahsten.texifyidea.util.addLabeledComponent
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import javax.swing.*

/**
 * Wrapper that contains the table creation dialog.
 *
 * @author Abby Berkers
 */
class TableCreationDialogWrapper(
    initialColumnTypes: List<ColumnType>? = null,
    initialTableModel: TableCreationTableModel? = null
) : DialogWrapper(true) {

    /**
     * Types of the columns of the table, see [ColumnType], always start with an empty table.
     */
    private val columnTypes = initialColumnTypes?.toMutableList() ?: mutableListOf()

    /**
     * The model of the table, always start with an empty table.
     */
    private val tableModel = initialTableModel ?: TableCreationTableModel()

    /**
     * The table component that shows the table.
     */
    private val table = JBTable(tableModel).apply {
        addTabCreatesNewRowAction()
        addEnterCreatesNewRowAction()
    }

    /**
     * The text field that contains the caption for the table.
     */
    private val txtCaption = JBTextField()

    /**
     * The text field that contains the label for the table. It has a default value "tab:" to encourage usage
     * of label conventions.
     */
    private val txtReference = JBTextField("tab:")

    /**
     * Information about the table that is needed to convert it to latex.
     */
    var tableInformation = TableInformation(tableModel, columnTypes, "", "")
        private set

    init {
        // Initialise the dialog, otherwise it shows as a line (i.e., infinitely small) and without any of the elements.
        init()
        title = "Insert Table"
    }

    /**
     * Add a table column.
     *
     * @param title of the column.
     * @param columnType is the column type of the column.
     */
    private fun addTableColumn(title: String, columnType: ColumnType) {
        // Add the column to the table, with an empty cell for each row (instead of the default null).
        tableModel.addColumn(title, (0 until tableModel.rowCount).map { "" }.toTypedArray())

        // Add the column type to the list of column types.
        columnTypes.add(columnType)

        // If table is currently empty, add one row to this new column.
        if (tableModel.columnCount == 1) {
            tableModel.addRow(arrayOf(""))
        }
    }

    /**
     * Edit the table column, i.e., udpate the header title and the column type.
     *
     * @param title is the new title of the header.
     * @param columnType is the index of the column type.
     * @param columnIndex is the index of the edited column in the table, starting at 0.
     */
    private fun editTableColumn(title: String, columnType: ColumnType, columnIndex: Int) {
        tableModel.setHeaderName(title, columnIndex)

        // Edit the column type of the edited column.
        columnTypes[columnIndex] = columnType

        tableModel.fireTableStructureChanged()
    }

    override fun createCenterPanel(): JPanel = JPanel(BorderLayout(8, 8)).apply {
        // Put help text below table
        add(createTablePanelContainer(), BorderLayout.CENTER)

        // Create labels.
        add(
            JPanel(VerticalLayout(8)).apply {
                addLabeledComponent(txtCaption, "Caption:", labelWidth = 64, leftPadding = 0)
                addLabeledComponent(txtReference, "Label:", labelWidth = 64, leftPadding = 0)
            },
            BorderLayout.SOUTH
        )
    }

    /**
     * Generates table and the toolbaar buttons.
     */
    private fun createToolbarDecorator() = ToolbarDecorator.createDecorator(table)
        .setAddAction {
            TableCreationEditColumnDialog(
                { title, columnType, _ -> addTableColumn(title, columnType) },
                tableModel.columnCount
            )
        }
        .setAddActionName("Add Column")
        .setAddIcon(addText(IconUtil.addIcon, "C"))
        .addExtraAction(getRemoveColumnActionButton() as AnAction)
        .addExtraAction(getEditColumnActionButton() as AnAction)
        .addExtraAction(getAddRowActionButton() as AnAction)
        .addExtraAction(
            getRemoveRowActionButton() as AnAction
        )
        .createPanel()

    /**
     * Panel containing the table and its controls.
     */
    private fun createTablePanel() = JPanel(BorderLayout()).apply {
        add(createToolbarDecorator(), BorderLayout.CENTER)
    }

    /**
     * Creates a hint label describing the table controls.
     */
    private fun createHelpText() = JBLabel().apply {
        text = "<html>Press tab to go to the next cell or row, press enter to go to the next row.</html>"
        foreground = JBColor.GRAY
    }

    /**
     * The panel containing everything related to the table.
     */
    private fun createTablePanelContainer() = JPanel(BorderLayout()).apply {
        minimumSize = Dimension(480, 320)

        add(createTablePanel(), BorderLayout.CENTER)
        add(createHelpText(), BorderLayout.SOUTH)
    }

    /**
     * See [IconUtil.addText].
     */
    fun addText(base: Icon, text: String, scale: Float = 7f): Icon = LayeredIcon(2).apply {
        setIcon(base, 0, SwingConstants.NORTH_WEST)
        setIcon(IconUtil.textToIcon(text, JLabel(), JBUIScale.scale(scale)), 1, SwingConstants.SOUTH_EAST)
    }

    /**
     * Sets the action when pressing TAB on the last cell in the last row to create a new (empty) row and set the
     * selection on the first cell of the new row.
     */
    private fun JTable.addTabCreatesNewRowAction() {
        // Get the key stroke for pressing TAB.
        val keyStroke = KeyStroke.getKeyStroke("TAB")
        val actionKey = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).get(keyStroke)

        // Get the action that currently is under the TAB key.
        val action = actionMap[actionKey]
        val actionWrapper = object : AbstractAction() {
            override fun actionPerformed(e: ActionEvent?) {
                val table = this@addTabCreatesNewRowAction

                // When we're in the last column of the last row, add a new row before calling the usual action.
                if (table.selectionModel.leadSelectionIndex == table.rowCount - 1 &&
                    table.columnModel.selectionModel.leadSelectionIndex == table.columnCount - 1
                ) {
                    tableModel.addEmptyRow()
                }

                // Perform the usual action.
                action.actionPerformed(e)
            }
        }

        // Map the new action to the TAB key.
        actionMap.put(actionKey, actionWrapper)
    }

    /**
     * Sets the action when pressing ENTER to create a new (empty) row and set the
     * selection on the first cell of the new row.
     *
     * See [addTabCreatesNewRowAction]
     */
    private fun JTable.addEnterCreatesNewRowAction() {
        val keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)
        getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keyStroke, "enter")

        val actionWrapper = object : AbstractAction() {
            override fun actionPerformed(e: ActionEvent?) {
                tableModel.addEmptyRow()

                val keyStrokeTab = KeyStroke.getKeyStroke("TAB")
                val actionKey = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).get(keyStrokeTab)
                // Get the action to go to the next cell
                val nextCellAction = actionMap[actionKey]

                // Skip the rest of the cells in the row
                val table = this@addEnterCreatesNewRowAction
                for (i in table.columnModel.selectionModel.leadSelectionIndex until table.columnCount) {
                    nextCellAction.actionPerformed(e)
                }
            }
        }

        actionMap.put("enter", actionWrapper)
    }

    /**
     * Saves all data in [tableInformation].
     * Every input is always valid: this will mean empty icons.
     */
    override fun doValidate(): ValidationInfo? {
        tableInformation = TableInformation(
            tableModel,
            columnTypes,
            txtCaption.text.trim(),
            txtReference.text.trim()
        )
        return null
    }

    private fun getEditColumnActionButton(): AnActionButton {
        return object : AnActionButton("Edit Column Header", addText(IconUtil.editIcon, "C")) {

            override fun isEnabled() = table.columnCount > 0

            override fun actionPerformed(e: AnActionEvent) {
                if (table.selectedColumn >= 0) {
                    TableCreationEditColumnDialog(
                        { title, columnType, columnIndex -> editTableColumn(title, columnType, columnIndex) },
                        table.selectedColumn,
                        table.getColumnName(table.selectedColumn),
                        columnTypes[table.selectedColumn]
                    )
                }
            }

            override fun getActionUpdateThread() = ActionUpdateThread.EDT
        }
    }

    private fun getAddRowActionButton(): AnActionButton {
        return object : AnActionButton("Add Row", addText(IconUtil.addIcon, "R")) {

            override fun isEnabled() = table.columnCount > 0

            override fun actionPerformed(e: AnActionEvent) {
                tableModel.addEmptyRow()
            }

            override fun getActionUpdateThread() = ActionUpdateThread.EDT
        }
    }

    private fun getRemoveRowActionButton(): AnActionButton {
        return object : AnActionButton("Remove Row", addText(IconUtil.removeIcon, "R")) {

            override fun isEnabled() = table.selectedRow > -1

            override fun actionPerformed(e: AnActionEvent) {
                tableModel.removeRow(table.selectedRow)
            }

            override fun getShortcut(): ShortcutSet {
                // Not sure if this is the way to set shortcuts, should we use the keymap?
                return ShortcutSet {
                    arrayOf(KeyboardShortcut(KeyStroke.getKeyStroke("DELETE"), null))
                }
            }

            override fun getActionUpdateThread() = ActionUpdateThread.EDT
        }
    }

    private fun getRemoveColumnActionButton(): AnActionButton {
        return object : AnActionButton("Remove Column", addText(IconUtil.removeIcon, "C")) {

            override fun isEnabled() = table.selectedColumn > -1

            override fun actionPerformed(e: AnActionEvent) {
                tableModel.removeColumn(table.selectedColumn)
            }

            override fun getActionUpdateThread() = ActionUpdateThread.EDT
        }
    }
}

package nl.hannahsten.texifyidea.action.wizard.table

import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.components.JBTextField
import com.intellij.ui.components.panels.VerticalLayout
import nl.hannahsten.texifyidea.util.addLabeledComponent
import javax.swing.JComboBox
import javax.swing.JPanel

/**
 * Dialog to add a new column to the table.
 *
 * @author Abby Berkers
 */
class TableCreationEditColumnDialog(

    /**
     * The function to execute when clicking the OK button.
     */
    private val onOkFunction: (title: String, columnType: ColumnType, columnIndex: Int) -> Unit,

    /**
     * The index of the column being edited.
     */
    private val editingColumn: Int,

    /**
     * The name of the column that is being edited. Default is the empty string, the title of a column that does
     * not yet exist.
     */
    private val columnName: String = "",

    /**
     * The [ColumnType] of the column that is being edited. Default is a text column.
     */
    private val columnType: ColumnType = ColumnType.TEXT_COLUMN
) {

    init {
        DialogBuilder().apply {
            // Text field for the name of the column, with the old name of the editing column filled in.
            val txtColumnName = JBTextField(columnName)

            // A combobox to select the column type.
            val cboxColumnType = JComboBox(ColumnType.values().map { it.displayName }.toTypedArray())
            cboxColumnType.selectedIndex = ColumnType.values().indexOf(columnType)

            // Add UI elements.
            val panel = JPanel(VerticalLayout(8)).apply {
                addLabeledComponent(txtColumnName, "Column title:", labelWidth = 96, leftPadding = 0)
                addLabeledComponent(cboxColumnType, "Column type:", labelWidth = 96, leftPadding = 0)
            }
            setCenterPanel(panel)
            setPreferredFocusComponent(txtColumnName)

            addOkAction()
            setOkOperation {
                dialogWrapper.close(0)
            }

            if (columnName.isBlank()) {
                title("Add Column")
            }
            else {
                title("Edit Column")
            }

            if (show() == DialogWrapper.OK_EXIT_CODE) {
                onOkFunction(txtColumnName.text, ColumnType.values()[cboxColumnType.selectedIndex], editingColumn)
            }
        }
    }
}

package nl.hannahsten.texifyidea.action.wizard.table

import java.util.*
import javax.swing.table.DefaultTableModel

/**
 * @author Abby Berkers
 */
class TableCreationTableModel : DefaultTableModel {

    constructor(data: Vector<Vector<Any?>>, columnNames: Vector<String>) : super(data, columnNames)

    constructor() : this(Vector(), Vector())

    /**
     * Remove a column and its header from the table.
     */
    fun removeColumn(column: Int) {
        // Remove the column from the data.
        dataVector.forEach {
            (it as Vector<*>).removeAt(column)
        }

        // Remove the header.
        columnIdentifiers.removeAt(column)

        // Let the table now that its structure has changed.
        fireTableStructureChanged()
    }

    /**
     * Set the name of a header.
     */
    fun setHeaderName(name: String, column: Int) {
        columnIdentifiers[column] = name
    }

    /**
     * Adds an empty row to the table.
     */
    fun addEmptyRow() {
        val emptyRow = Array(columnCount) { "" }
        addRow(emptyRow)
    }

    /**
     * Get the names of the columns.
     */
    fun getColumnNames(): Vector<Any> = columnIdentifiers
}

package nl.hannahsten.texifyidea.action.wizard.table

/**
 * Stores all information about a table.
 *
 * @author Abby Berkers
 */
data class TableInformation(

    /**
     * Contains all information about the contents of the table.
     * That is, the column names and the table entries.
     */
    val tableModel: TableCreationTableModel,

    /**
     * Contains the type of each column.
     */
    val columnTypes: List<ColumnType>,

    /**
     * Contains the caption to go along with the table.
     */
    val caption: String,

    /**
     * Contains the label that is to be used to reference the table.
     */
    val label: String
)

package nl.hannahsten.texifyidea.inspections.latex.codematurity

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.insertUsepackage
import nl.hannahsten.texifyidea.util.parser.name
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexAvoidEqnarrayInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "AvoidEqnarray"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Avoid eqnarray"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val environments = file.childrenOfType(LatexEnvironment::class)
        for (env in environments) {
            val name = env.name()?.text ?: continue
            if (name != "eqnarray" && name != "eqnarray*") {
                continue
            }

            val star = name.substring("eqnarray".length)
            if (env.endCommand != null) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        env,
                        TextRange.from(7, name.length),
                        "Avoid using the 'eqnarray$star' environment",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        ChangeEnvironmentFix(star)
                    )
                )
            }
        }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    private open class ChangeEnvironmentFix(val star: String) : LocalQuickFix {

        override fun getFamilyName() = "Convert to 'align$star' (amsmath)"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val environment = descriptor.psiElement as LatexEnvironment
            val file = environment.containingFile
            val document = file.document() ?: return
            val begin = environment.beginCommand
            val end = environment.endCommand ?: return

            document.replaceString(end.textOffset, end.endOffset(), "\\end{align$star}")
            document.replaceString(begin.textOffset, begin.endOffset(), "\\begin{align$star}")

            file.insertUsepackage(AMSMATH)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codematurity

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.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexParameter
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.nextSiblingIgnoreWhitespace
import java.util.*
import kotlin.math.max

/**
 * For now, only not using it before `\ref` or `\cite` will be detected.
 *
 * @author Hannah Schellekens
 */
open class LatexDiscouragedUseOfDefInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "DiscouragedUseOfDef"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Use \\(re)newcommand instead of \\let and \\def"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.commandsInFile()
        for (cmd in commands) {
            // Only consider \let and \def.
            if (cmd.name == "\\let" || cmd.name == "\\def") {
                descriptors.add(
                    manager.createProblemDescriptor(
                        cmd,
                        TextRange(0, cmd.textLength),
                        "The use of TeX primitive ${cmd.name} is discouraged",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        NewcommandFix(),
                        NewcommandFix("\\renewcommand")
                    )
                )
            }
        }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    private open class NewcommandFix(val commandName: String) : LocalQuickFix {

        constructor() : this("\\newcommand")

        override fun getFamilyName() = "Convert to $commandName"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val file = command.containingFile
            val document = file.document() ?: return
            val (cmd, value) = getArguments(command) ?: return

            val startOFfset = command.textOffset
            val endOffset = max(cmd.textOffset + cmd.textLength, value.textOffset + value.textLength)

            val valueText = if (value.text.startsWith("{") && value.text.endsWith("}")) value.text.drop(1).dropLast(1) else value.text

            document.replaceString(startOFfset, endOffset, "$commandName{${cmd.text}}{$valueText}")
        }

        open fun getArguments(command: LatexCommands): Pair<PsiElement, PsiElement>? {
            val parent = command.parent
            val definedCommand = parent.nextSiblingIgnoreWhitespace()?.firstChildOfType(LatexCommands::class) ?: return null
            // Either the definition is wrapped in braces, in which case it will be parsed as a parameter of the command being defined, or it is a sibling of the command
            val value = definedCommand.firstChildOfType(LatexParameter::class)
                ?: definedCommand.nextSiblingIgnoreWhitespace()
                ?: definedCommand.parent.nextSiblingIgnoreWhitespace()
                ?: return null
            return Pair(definedCommand.commandToken, value)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codematurity

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import java.util.*
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * Best would be if we could detect when the usage of \makeatletter/\makeatother is unnecessary, but this is practically impossible.
 *
 * @author Hannah Schellekens
 */
open class LatexMakeatletterInspection : TexifyRegexInspection(
    inspectionDisplayName = "Discouraged use of \\makeatletter in tex sources",
    inspectionId = "Makeatletter",
    highlight = ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
    errorMessage = { "${it.group(1)} should only be used when necessary" },
    pattern = Pattern.compile("(\\\\makeatletter|\\\\makeatother)"),
    replacement = { _, _ -> "" },
    quickFixName = { "Remove command" }
) {

    override fun checkContext(matcher: Matcher, element: PsiElement): Boolean {
        val file = element.containingFile
        val extension = file.virtualFile.extension
        return extension?.lowercase(Locale.getDefault()) == "tex" && super.checkContext(matcher, element)
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codematurity

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.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexNoMathContent
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.parser.nextSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.previousSiblingIgnoreWhitespace
import org.jetbrains.annotations.Nls
import java.util.*

/**
 * @author Hannah Schellekens
 */
class LatexOverInsteadOfFracInspection : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup
        get() = InsightGroup.LATEX

    @Nls
    override fun getDisplayName(): String {
        return "Discouraged use of \\over"
    }

    override val inspectionId: String
        get() = "OverInsteadOfFrac"

    override val outerSuppressionScopes: Set<MagicCommentScope>
        get() = EnumSet.of(MagicCommentScope.COMMAND)

    override fun inspectFile(
        file: PsiFile,
        manager: InspectionManager,
        isOntheFly: Boolean
    ): List<ProblemDescriptor> {
        val descriptors = descriptorList()
        val commands = LatexCommandsIndex.getItems(file)
        for (command in commands) {
            if ("\\over" == command.name) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        command,
                        "Use of \\over is discouraged",
                        OverToFracFix(),
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }
        }
        return descriptors
    }

    private inner class OverToFracFix : LocalQuickFix {

        @Nls
        override fun getFamilyName(): String {
            return "Convert to \\frac"
        }

        override fun applyFix(
            project: Project,
            descriptor: ProblemDescriptor
        ) {
            val element = descriptor.psiElement as? LatexCommands ?: return

            // Find elements to put in numerator and denominator.
            val content = element.parentOfType(LatexNoMathContent::class)
            val previous = content!!.previousSiblingIgnoreWhitespace()
            val next = content.nextSiblingIgnoreWhitespace()
            val before = previous?.text ?: ""
            val after = next?.text ?: ""
            val psiReplacement = LatexPsiHelper(project).createFromText("\\frac{$before}{$after}").firstChild

            // Add the replacement in the psi tree.
            val bla = next ?: content
            bla.parent.addAfter(psiReplacement, bla)
            // Remove the old fraction (numerator\over denominator), including a possible space between \over and the
            // denominator. Remove this space before removing the \over command so content still exists when removing
            // its next sibling. If there is no space after \over, this will remove the denominator (next) and removing
            // next does nothing.
            if (next != null) {
                content.nextSibling.delete()
                next.delete()
            }
            previous?.delete()
            content.delete()
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codematurity

import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.toTextRange
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexPrimitiveEquationInspection : TexifyRegexInspection(
    inspectionDisplayName = "Discouraged use of primitive TeX display math",
    inspectionId = "PrimitiveEquation",
    errorMessage = { "Use '\\[..\\]' instead of primitive TeX display math." },
    pattern = Pattern.compile("(\\\$\\\$)[^\$]*\\\$?[^\$]*(\\\$\\\$)"),
    mathMode = false,
    replacement = { _, _ -> "" },
    replacementRange = this::replaceRange,
    highlight = ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
    highlightRange = { replaceRange(it).toTextRange().grown(-1) },
    quickFixName = { "Replace with '\\[..\\]'" }
) {

    companion object {

        fun replaceRange(it: Matcher) = (it.groupRange(1).first..it.groupRange(2).last)
    }

    override fun applyFix(descriptor: ProblemDescriptor, replacementRange: IntRange, replacement: String, groups: List<String>): Int {
        val file = descriptor.psiElement as PsiFile
        val document = file.document() ?: return 0

        document.replaceString(replacementRange.first, replacementRange.first + 2, "\\[")
        document.replaceString(replacementRange.last - 2, replacementRange.last, "\\]")

        // $$ were replaced by \[ or \] so the length did not change
        return 0
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codematurity

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.psi.util.siblings
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexGroup
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import org.jetbrains.annotations.Nls

/**
 * @author Hannah Schellekens
 */
class LatexPrimitiveStyleInspection : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup
        get() = InsightGroup.LATEX

    @Nls
    override fun getDisplayName(): String {
        return "Discouraged use of TeX styling primitives"
    }

    override val inspectionId: String
        get() = "PrimitiveStyle"

    override fun inspectFile(
        file: PsiFile,
        manager: InspectionManager,
        isOntheFly: Boolean
    ): List<ProblemDescriptor> {
        val descriptors = mutableListOf<ProblemDescriptor>()
        val commands = LatexCommandsIndex.getItems(file)
        for (command in commands) {
            val index = CommandMagic.stylePrimitives.indexOf(command.name)
            if (index < 0) {
                continue
            }
            descriptors.add(
                manager.createProblemDescriptor(
                    command,
                    "Use of TeX primitive " + CommandMagic.stylePrimitives[index] + " is discouraged",
                    InspectionFix(SmartPointerManager.createPointer(command)),
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )
        }
        return descriptors
    }

    private inner class InspectionFix(val oldCommand: SmartPsiElementPointer<LatexCommands>) : LocalQuickFixAndIntentionActionOnPsiElement(oldCommand.element) {

        @Nls
        override fun getFamilyName(): String {
            return "Convert to LaTeX alternative"
        }

        override fun getText(): String {
            return oldCommand.element
                ?.let { "Convert ${it.name} to ${CommandMagic.stylePrimitiveReplacements[it.name]}" }
                ?: familyName
        }

        override fun invoke(project: Project, file: PsiFile, editor: Editor?, startElement: PsiElement, endElement: PsiElement) {
            val command = startElement as? LatexCommands ?: return
            val newCommandName = CommandMagic.stylePrimitiveReplacements[command.name] ?: return

            // Convert {help \bf this is content} to "help \textbf{this is content}", but leave \bf{this is bold} alone.
            if (command.getRequiredParameters().isEmpty()) {
                val commandInFile = file.findElementAt(oldCommand.range?.startOffset ?: return) ?: return

                // Find the LaTeX group surrounding this command, so we can separate its content in all the siblings
                // before the command and all the siblings after the command.
                val previousTrace = mutableListOf(commandInFile)
                while (previousTrace.last().parent != null) {
                    val parent = previousTrace.last().parent
                    previousTrace.add(parent)
                    if (parent is LatexGroup) break
                }

                val commandInContent = previousTrace.lastOrNull { it.text == commandInFile.text }
                val prefixContent = commandInContent?.siblings(forward = false, withSelf = false)?.toList()
                    ?.reversed()
                    ?.joinToString(" ") { it.text }
                    ?.trim()
                    ?.let { if (it.isBlank()) "" else "$it " }
                    ?: ""
                val requiredParamContent = commandInContent?.siblings(forward = true, withSelf = false)
                    ?.joinToString(" ") { it.text }
                    ?.trim() ?: ""

                val newPsi = LatexPsiHelper(project).createFromText(
                    "$prefixContent$newCommandName{$requiredParamContent}"
                ).firstChild

                val parentGroup = previousTrace.last()
                if (parentGroup is LatexGroup) {
                    parentGroup.replace(newPsi)
                }
                // The \bf is not inside a group, so rename the command instead.
                else {
                    command.setName(newCommandName)
                }
            }
            // Convert \bf{this is bold} to \textbf{this is bold}
            else {
                command.setName(newCommandName)
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.getEnvironmentName
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import org.jetbrains.annotations.Nls

class LatexDocumentclassNotInRootInspection : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup
        get() = InsightGroup.LATEX

    @Nls
    override fun getDisplayName(): String {
        return "Documentclass command should be in the same file as the document environment"
    }

    override val inspectionId: String
        get() = "DocumentclassNotInRoot"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val documentClass = file.commandsInFile().find { it.name == "\\documentclass" } ?: return emptyList()

        val hasDocumentEnvironment = file.childrenOfType<LatexEnvironment>().any { it.getEnvironmentName() == "document" }

        if (!hasDocumentEnvironment) {
            return listOf(
                manager.createProblemDescriptor(
                    documentClass,
                    displayName,
                    true,
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )
        }
        return emptyList()
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.labels.findLatexLabelingElementsInFileSet
import nl.hannahsten.texifyidea.util.parser.findOuterMathEnvironment
import nl.hannahsten.texifyidea.util.insertUsepackage
import java.util.regex.Pattern

open class LatexEquationReferenceInspection : TexifyRegexInspection(
    inspectionDisplayName = "Use of (\\ref{...}) instead of \\eqref{...}",
    inspectionId = "EquationReference",
    errorMessage = { "Use \\eqref" },
    pattern = Pattern.compile("(\\(\\\\ref\\{)([\\w:]+)(}\\))"),
    quickFixName = { "Replace with \\eqref" },
    groupFetcher = { listOf(it.group(2)) },
    cancelIf = { matcher, psiFile ->
        // Cancel if the label was defined outside a math environment.
        psiFile.findLatexLabelingElementsInFileSet().find { it.text == "\\label{${matcher.group(2)}}" }.findOuterMathEnvironment() == null
    }
) {

    override fun applyFix(descriptor: ProblemDescriptor, replacementRange: IntRange, replacement: String, groups: List<String>): Int {
        val file = descriptor.psiElement as PsiFile
        val document = file.document() ?: return 0
        val reference = groups[0]

        document.replaceString(replacementRange.first, replacementRange.last, "\\eqref{$reference}")
        // We add two characters: 'eq' and remove the two braces, thus the document gets 0 characters longer.
        return 0
    }

    override fun applyFixes(descriptor: ProblemDescriptor, replacementRanges: List<IntRange>, replacements: List<String>, groups: List<List<String>>) {
        super.applyFixes(descriptor, replacementRanges, replacements, groups)

        // We overrode applyFixes instead of applyFix because all fixes need to be applied together, and only after that we insert any required package.
        val file = descriptor.psiElement.containingFile ?: return
        file.insertUsepackage(LatexPackage.AMSMATH)
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.createSmartPointer
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.SafeDeleteFix
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.isFigureLabel
import nl.hannahsten.texifyidea.util.labels.findLabelingCommandsInFile
import nl.hannahsten.texifyidea.util.labels.getLabelReferenceCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import java.util.*

open class LatexFigureNotReferencedInspection : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup = InsightGroup.LATEX

    override val inspectionId: String = "FigureNotReferenced"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName(): String = "Figure not referenced"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val figureLabels = getFigureLabels(file)

        removeReferencedLabels(file, figureLabels)

        val descriptors = descriptorList()
        for (label in figureLabels.values) {
            descriptors.add(createDescriptor(manager, label, isOntheFly) ?: continue)
        }

        return descriptors
    }

    private fun removeReferencedLabels(file: PsiFile, figureLabels: MutableMap<String?, LatexCommands>) {
        val referenceCommands = file.project.getLabelReferenceCommands()
        for (command in file.commandsInFileSet()) {
            // Don't resolve references in command definitions
            if (command.parent?.firstParentOfType(LatexCommands::class)?.name in CommandMagic.commandDefinitionsAndRedefinitions ||
                referenceCommands.contains(command.name)
            ) {
                command.referencedLabelNames.forEach { figureLabels.remove(it) }
            }
        }
    }

    private fun createDescriptor(manager: InspectionManager, label: LatexCommands, isOntheFly: Boolean): ProblemDescriptor? =
        label.firstChildOfType(LatexParameterText::class)?.let {
            manager.createProblemDescriptor(
                it,
                "Figure is not referenced",
                RemoveFigureFix(it.createSmartPointer()),
                ProblemHighlightType.LIKE_UNUSED_SYMBOL,
                isOntheFly
            )
        }

    /**
     * Find all commands in the file that label a figure.
     */
    private fun getFigureLabels(file: PsiFile): MutableMap<String?, LatexCommands> =
        file.findLabelingCommandsInFile()
            .filter(LatexCommands::isFigureLabel)
            .associateBy(LatexCommands::labelName)
            .toMutableMap()

    class RemoveFigureFix(label: SmartPsiElementPointer<LatexParameterText>) : SafeDeleteFix(label.element as PsiElement) {

        override fun getText(): String {
            return "Safe delete figure environment"
        }
    }
}

private val LatexCommands.labelName: String?
    get() = requiredParameter(0)

private val LatexCommands.referencedLabelNames: List<String>
    get() = requiredParameter(0)?.split(",") ?: emptyList()

fun dummy() = Unit

package nl.hannahsten.texifyidea.inspections.latex.codestyle

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.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSMATH
import nl.hannahsten.texifyidea.psi.LatexNoMathContent
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.*

/**
 * @author Hannah Schellekens
 */
open class LatexGatherEquationsInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override fun getDisplayName() = "Gather equations"

    override val inspectionId = "GatherEquations"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        file.childrenOfType(LatexNoMathContent::class).asSequence()
            .filter { it.isDisplayMath() }
            .map { Pair(it, it.nextSiblingIgnoreWhitespace()) }
            .filter { (_, next) -> next != null && next is LatexNoMathContent && next.isDisplayMath() }
            .flatMap { sequenceOf(it.first, it.second) }
            .distinct()
            .forEach {
                descriptors.add(
                    manager.createProblemDescriptor(
                        it ?: return@forEach,
                        "Equations can be gathered",
                        GatherEnvironments(),
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    private class GatherEnvironments : LocalQuickFix {

        override fun getFamilyName() = "Gather equations"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val element = descriptor.psiElement as? LatexNoMathContent ?: return
            val file = element.containingFile
            val document = file.document() ?: return

            val equations = findEquations(element)
            val startOffset = equations.first().textOffset
            val endOffset = equations.last().endOffset()
            val indent = document.lineIndentationByOffset(startOffset)

            // Replace content.
            val gather = buildString {
                append("\\begin{gather*}\n")

                equations.asSequence()
                    .map { trimEquation(it) }
                    .forEach {
                        append(indent)
                        append("    ")
                        append(it)
                        append("\\\\\n")
                    }

                append(indent)
                append("\\end{gather*}")
            }
            document.replaceString(startOffset, endOffset, gather)

            // Add import.
            PackageUtils.insertUsepackage(file, AMSMATH)
        }

        /**
         * Trims the `\[`, `\]` and whitespace from an equations.
         *
         * @param equation
         *          The equation to trim, must be display math, will not be checked by this method.
         */
        private fun trimEquation(equation: LatexNoMathContent) = equation.text
            // Remove \[ and \]
            .trimRange(2, 2)
            // Remove whitespace
            .trim()

        /**
         * Finds all adjacent equations.
         */
        private fun findEquations(base: LatexNoMathContent): List<LatexNoMathContent> {
            val equations = ArrayList<LatexNoMathContent>()
            equations.add(base)

            // Lookbehind.
            var content = base.previousSiblingIgnoreWhitespace()
            while (content is LatexNoMathContent && content.isDisplayMath()) {
                equations.add(0, content)
                content = content.previousSiblingIgnoreWhitespace()
            }

            // Lookahead.
            content = base.nextSiblingIgnoreWhitespace()
            while (content is LatexNoMathContent && content.isDisplayMath()) {
                equations.add(content)
                content = content.nextSiblingIgnoreWhitespace()
            }

            return equations
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.parentOfType
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexGroupedSubSupScriptInspection : TexifyRegexInspection(
    inspectionDisplayName = "Grouped superscript and subscript",
    inspectionId = "GroupedSubSupScript",
    errorMessage = {
        val subSup = if (it.group(1) == "_") "Sub" else "Super"
        "${subSup}script is not grouped"
    },
    pattern = Pattern.compile("((?<!(\\\\)|(\\\\string))[_^])([a-zA-Z0-9][a-zA-Z0-9]+)"),
    mathMode = true,
    highlightRange = { TextRange(it.start() - 1, it.end()) },
    replacement = { it, _ -> "{${it.group(4)}}" },
    replacementRange = { it.groupRange(4) },
    quickFixName = { "Insert curly braces" }
) {

    override fun checkContext(element: PsiElement): Boolean {
        val parent = element.parentOfType(LatexCommands::class)
        return when (parent?.name) {
            "\\label", "\\bibitem" -> false
            else -> super.checkContext(element)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

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.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.settings.conventions.LabelConventionType
import nl.hannahsten.texifyidea.settings.conventions.TexifyConventionsSettingsManager
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.commandsAndFilesInFileSet
import nl.hannahsten.texifyidea.util.labels.extractLabelName
import nl.hannahsten.texifyidea.util.labels.findLatexAndBibtexLabelStringsInFileSet
import nl.hannahsten.texifyidea.util.labels.findLatexLabelingElementsInFile
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * Check for label conventions, e.g. sec: in \section{A section}\label{sec:a-section}
 *
 * @author Hannah Schellekens
 */
open class LatexLabelConventionInspection : TexifyInspectionBase() {

    companion object {

        private fun getLabeledCommand(label: PsiElement): PsiElement? {
            return when (label) {
                is LatexCommands -> {
                    if (CommandMagic.labelAsParameter.contains(label.name)) {
                        return label
                    }

                    if (label.inDirectEnvironmentMatching {
                        val conventionSettings = TexifyConventionsSettingsManager
                            .getInstance(label.project).getSettings()
                        conventionSettings.getLabelConvention(
                                it.getEnvironmentName(),
                                LabelConventionType.ENVIRONMENT
                            ) != null &&
                            !EnvironmentMagic.labelAsParameter.contains(it.getEnvironmentName())
                    }
                    ) {
                        label.parentOfType(LatexEnvironment::class)
                    }
                    else {
                        val parent = label.parentOfType(LatexNoMathContent::class) ?: return null
                        val sibling = parent.previousSiblingIgnoreWhitespace() ?: return null
                        sibling.firstChildOfType(LatexCommands::class)
                    }
                }
                is LatexEnvironment -> {
                    label
                }
                else -> null
            }
        }

        /**
         * Finds the expected prefix for the supplied label
         */
        private fun getLabelPrefix(labeledCommand: PsiElement): String? {
            val conventionSettings =
                TexifyConventionsSettingsManager.getInstance(labeledCommand.project).getSettings()

            return when (labeledCommand) {
                is LatexCommands -> {
                    conventionSettings.getLabelConvention(
                        labeledCommand.name,
                        LabelConventionType.COMMAND
                    )?.prefix
                }
                is LatexEnvironment -> {
                    conventionSettings.getLabelConvention(
                        labeledCommand.getEnvironmentName(),
                        LabelConventionType.ENVIRONMENT
                    )?.prefix
                }
                else -> null
            }
        }
    }

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "LabelConvention"

    override val ignoredSuppressionScopes = EnumSet.of(MagicCommentScope.COMMAND, MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Label conventions"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = mutableListOf<ProblemDescriptor>()
        checkLabels(file, manager, isOntheFly, descriptors)
        return descriptors
    }

    private fun checkLabels(
        file: PsiFile, manager: InspectionManager, isOntheFly: Boolean,
        descriptors: MutableList<ProblemDescriptor>
    ) {
        file.findLatexLabelingElementsInFile().forEach { label ->
            val labeledCommand = getLabeledCommand(label) ?: return@forEach
            val expectedPrefix = getLabelPrefix(labeledCommand)
            val labelName = label.extractLabelName()
            if (!expectedPrefix.isNullOrBlank() && !labelName.startsWith("$expectedPrefix:")) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        label,
                        "Unconventional label prefix",
                        LabelPreFix(),
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }
        }
    }

    /**
     * @author Hannah Schellekens
     */
    private class LabelPreFix : LocalQuickFix {

        override fun getFamilyName() = "Fix label name"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement
            val baseFile = command.containingFile
            val oldLabel = command.extractLabelName()
            val latexPsiHelper = LatexPsiHelper(project)
            val labeledCommand = getLabeledCommand(command) ?: return

            // Determine label name.
            val prefix: String = getLabelPrefix(labeledCommand) ?: return
            val labelName = oldLabel.formatAsLabel()
            val createdLabelBase = if (labelName.contains(":")) {
                PatternMagic.labelPrefix.matcher(labelName).replaceAll("$prefix:")
            }
            else {
                "$prefix:$labelName"
            }

            val createdLabel = appendCounter(createdLabelBase, baseFile.findLatexAndBibtexLabelStringsInFileSet())

            // Replace in command label definition
            if (command is LatexCommands) {
                if (CommandMagic.labelAsParameter.contains(command.name)) {
                    latexPsiHelper.setOptionalParameter(command, "label", "{$createdLabel}")
                }
                else {
                    val labelInfo = CommandManager.labelAliasesInfo.getOrDefault(command.name, null) ?: return
                    if (!labelInfo.labelsPreviousCommand) return
                    val position = labelInfo.positions.firstOrNull() ?: return

                    val labelParameter = command.requiredParameters().getOrNull(position) ?: return
                    labelParameter.replace(latexPsiHelper.createRequiredParameter(createdLabel))
                }
            }

            // Replace in environment
            if (command is LatexEnvironment) {
                latexPsiHelper.setOptionalParameter(command.beginCommand, "label", "{$createdLabel}")
            }

            // Replace in document.
            val filesAndReferences = findReferences(baseFile, oldLabel)
            for (pair in filesAndReferences) {
                val references = pair.second
                for (reference in references) {
                    reference.replace(latexPsiHelper.createRequiredParameter(createdLabel))
                }
            }
        }

        /**
         * Find all references to label `labelName`.
         */
        private fun findReferences(
            file: PsiFile,
            labelName: String
        ): MutableSet<Pair<PsiFile, MutableList<LatexRequiredParam>>> {
            val result = mutableSetOf<Pair<PsiFile, MutableList<LatexRequiredParam>>>()

            val commandsAndFiles = file.commandsAndFilesInFileSet()

            // Loop over every file
            for (pair in commandsAndFiles) {
                // Only look at commands which refer to something
                val commands =
                    pair.second.filter { CommandMagic.labelReferenceWithoutCustomCommands.contains(it.name) }.reversed()
                val requiredParams = mutableListOf<LatexRequiredParam>()

                // Find all the parameters with the given labelName
                for (ref in commands) {
                    val parameters = ref.requiredParameters()
                        .filter { p -> p.firstChildOfType(LatexParameterText::class)?.text == labelName }
                    requiredParams.addAll(parameters)
                }

                result.add(Pair(pair.first, requiredParams))
            }

            return result
        }

        /**
         * Keeps adding a counter behind the label until there is no other label with that name.
         */
        private fun appendCounter(label: String, allLabels: Set<String>): String {
            var counter = 2
            var candidate = label

            while (allLabels.contains(candidate)) {
                candidate = label + (counter++)
            }

            return candidate
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.inspections.latex.typesetting.spacing.LatexSpaceAfterAbbreviationInspection
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.magic.PatternMagic.sentenceEndPrefix
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.parser.isComment
import nl.hannahsten.texifyidea.util.parser.nextSiblingIgnoreWhitespace
import kotlin.math.min

/**
 * @author Hannah Schellekens
 */
open class LatexLineBreakInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override fun getDisplayName() = "Start sentences on a new line"

    override val inspectionId = "LineBreak"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()
        val document = file.document() ?: return descriptors

        // Target all regular text for this inspection.
        val texts = file.childrenOfType(LatexNormalText::class)
        for (text in texts) {
            // Do not trigger the inspection in math mode.
            if (text.inMathContext()) {
                continue
            }

            val matcher = PatternMagic.sentenceEnd.matcher(text.text)
            while (matcher.find()) {
                val offset = text.textOffset + matcher.end()
                val lineNumber = document.getLineNumber(offset)
                val endLine = document.getLineEndOffset(lineNumber)

                val startOffset = matcher.start()
                val endOffset = matcher.end() + (endLine - offset)

                val element = file.findElementAt(startOffset + text.startOffset)
                // Do not trigger the inspection when in a comment or when a comment starts directly after.
                if ((element?.isComment() == true) || (element?.nextSiblingIgnoreWhitespace()?.isComment() == true)) {
                    continue
                }

                // It may be that this inspection is incorrectly triggered on an abbreviation.
                // However, that means that the correct user action is to write a normal space after the abbreviation,
                // which is what we suggest with this quickfix.
                val dotPlusSpace = "^$sentenceEndPrefix(\\.\\s)".toRegex().find(text.text.substring(startOffset, matcher.end()))?.groups?.get(0)?.range?.shiftRight(startOffset + 1)
                val normalSpaceFix = if (dotPlusSpace != null) LatexSpaceAfterAbbreviationInspection.NormalSpaceFix(dotPlusSpace) else null
                val fixes = listOfNotNull(InspectionFix(), normalSpaceFix).toTypedArray()

                descriptors.add(
                    manager.createProblemDescriptor(
                        text,
                        TextRange(startOffset, min(text.textLength, endOffset)),
                        "Sentence does not start on a new line",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        *fixes
                    )
                )
            }
        }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    private class InspectionFix : LocalQuickFix {

        override fun getFamilyName() = "Insert line feed"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val textElement = descriptor.psiElement
            val document = textElement.containingFile.document() ?: return
            val range = descriptor.textRangeInElement
            val textOffset = textElement.textOffset
            val textInElement = document.getText(TextRange(textOffset + range.startOffset, textOffset + range.endOffset))

            // Do not apply the fix when there is no break point.
            val signMarker = PatternMagic.sentenceSeparator.matcher(textInElement)
            if (signMarker.find().not()) {
                return
            }

            // Fill in replacement
            val offset = textElement.textOffset + descriptor.textRangeInElement.startOffset + signMarker.end() + 1
            val lineNumber = document.getLineNumber(offset)
            val replacement = "\n${document.lineIndentation(lineNumber)}"
            document.insertString(offset, replacement)
            document.deleteString(offset - 1, offset)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

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 com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import nl.hannahsten.texifyidea.util.parser.requiredParameters

/**
 * @author Hannah Schellekens
 */
open class LatexMathFunctionTextInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override fun getDisplayName() = "Use math function instead of \\text"

    override val inspectionId = "MathFunctionText"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        file.commandsInFile("\\text").asSequence()
            .filter { it.inMathContext() }
            .filter { it.requiredParameter(0)?.trim() in AFFECTED_COMMANDS }
            .forEach { affectedTextCommand ->
                descriptors.add(
                    manager.createProblemDescriptor(
                        affectedTextCommand,
                        "Use math function instead of \\text",
                        MathFunctionFix(SmartPointerManager.createPointer(affectedTextCommand)),
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    private class MathFunctionFix(val textCommandPointer: SmartPsiElementPointer<LatexCommands>) : LocalQuickFix {

        override fun getFamilyName() = "Convert to math function"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val textCommand = textCommandPointer.element ?: return
            val document = textCommand.containingFile.document() ?: return
            val mathFunction = textCommand.requiredParameter(0)?.trim() ?: return
            document.replaceString(textCommand.startOffset, textCommand.requiredParameters()[0].endOffset, "\\$mathFunction")
        }
    }

    companion object {

        private val AFFECTED_COMMANDS = CommandMagic.slashlessMathOperators.asSequence()
            .map { it.command }
            .toSet()
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.commands.LatexNewDefinitionCommand
import nl.hannahsten.texifyidea.util.parser.forcedFirstRequiredParameterAsCommand
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.cmd
import org.jetbrains.annotations.Nls

/**
 * @author Sten Wessel
 */
class LatexMightBreakTexifyInspection : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup
        get() = InsightGroup.LATEX

    @Nls
    override fun getDisplayName(): String {
        return "Might break TeXiFy functionality"
    }

    override val inspectionId: String
        get() = "MightBreakTexify"

    override fun inspectFile(
        file: PsiFile,
        manager: InspectionManager,
        isOntheFly: Boolean
    ): List<ProblemDescriptor> {
        val descriptors = descriptorList()
        val commands = LatexCommandsIndex.getItems(file)
        for (command in commands) {
            // Error when \newcommand is used on existing command
            if (CommandMagic.commandRedefinitions.contains(command.name)) {
                val newCommand = command.forcedFirstRequiredParameterAsCommand()
                if (CommandMagic.fragile.contains(newCommand?.name) || command.name == LatexNewDefinitionCommand.CATCODE.cmd) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            "Redefining ${newCommand?.name ?: "this command"} might break TeXiFy functionality",
                            null as LocalQuickFix?,
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                        )
                    )
                }
            }
        }
        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.createSmartPointer
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.intentions.LatexAddLabelToCommandIntention
import nl.hannahsten.texifyidea.intentions.LatexAddLabelToEnvironmentIntention
import nl.hannahsten.texifyidea.lang.LatexDocumentClass
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.getEnvironmentName
import nl.hannahsten.texifyidea.psi.getLabel
import nl.hannahsten.texifyidea.settings.conventions.LabelConventionType
import nl.hannahsten.texifyidea.settings.conventions.TexifyConventionsConfigurable
import nl.hannahsten.texifyidea.settings.conventions.TexifyConventionsSettingsManager
import nl.hannahsten.texifyidea.util.files.*
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.hasLabel
import nl.hannahsten.texifyidea.util.parser.hasStar
import org.jetbrains.annotations.Nls
import java.util.*

/**
 * Check for commands which should have a label but don't.
 *
 * @author Hannah Schellekens
 */
open class LatexMissingLabelInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "MissingLabel"

    override val ignoredSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Missing labels"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val settings = TexifyConventionsSettingsManager.getInstance(file.project).getSettings()

        val requireLabel = settings.currentScheme.labelConventions.filter { it.enabled }
        val labeledCommands =
            requireLabel.filter { c -> c.type == LabelConventionType.COMMAND }.map { "\\" + it.name }.toMutableSet()

        // Document classes like book and report provide \part as sectioning, but with exam class it's a part in a question
        if (file.findRootFile().documentClass() == LatexDocumentClass.EXAM.name) {
            labeledCommands.remove("\\part")
        }

        file.commandsInFile().filter {
            labeledCommands.contains(it.name) && it.name != "\\item" && !it.hasStar()
        }.forEach { addCommandDescriptor(it, descriptors, manager, isOntheFly) }

        val labeledEnvironments =
            requireLabel.filter { c -> c.type == LabelConventionType.ENVIRONMENT }.map { it.name }.toSet()
        file.environmentsInFile().filter { env -> labeledEnvironments.contains(env.getEnvironmentName()) }
            .forEach { addEnvironmentDescriptor(it, descriptors, manager, isOntheFly) }

        return descriptors
    }

    /**
     * Adds a command descriptor to the given command if there is a label missing.
     *
     * @return `true` when a descriptor was added, or `false` when no descriptor was added.
     */
    private fun addCommandDescriptor(
        command: LatexCommands, descriptors: MutableList<ProblemDescriptor>,
        manager: InspectionManager, isOntheFly: Boolean
    ): Boolean {
        if (command.hasLabel()) {
            return false
        }

        val fixes = mutableListOf<LocalQuickFix>()
        fixes.add(InsertLabelForCommandFix())
        if (!CommandMagic.labelAsParameter.contains(command.name)) {
            fixes.add(ChangeMinimumLabelLevelFix())
        }

        // For adding the label, see LatexAddLabelIntention
        descriptors.add(
            manager.createProblemDescriptor(
                command,
                "Missing label",
                fixes.toTypedArray(),
                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                isOntheFly,
                false
            )
        )

        return true
    }

    private fun addEnvironmentDescriptor(
        environment: LatexEnvironment, descriptors: MutableList<ProblemDescriptor>,
        manager: InspectionManager, isOntheFly: Boolean
    ): Boolean {
        if (environment.getLabel() != null) {
            return false
        }

        descriptors.add(
            manager.createProblemDescriptor(
                environment,
                "Missing label",
                arrayOf(InsertLabelInEnvironmentFix()),
                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                isOntheFly,
                false
            )
        )

        return true
    }

    /**
     * Open the settings page so the user can change the minimum labeled level.
     */
    private class ChangeMinimumLabelLevelFix : LocalQuickFix {

        @Nls
        override fun getFamilyName(): String {
            return "Change label conventions"
        }

        override fun startInWriteAction() = false

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            ShowSettingsUtil.getInstance().showSettingsDialog(project, TexifyConventionsConfigurable::class.java)
        }
    }

    /**
     * This is also an intention, but in order to keep the same alt+enter+enter functionality (because we have an other
     * quickfix as well) we keep it as a quickfix also.
     */
    private class InsertLabelForCommandFix : LocalQuickFix {

        // It has to appear in alphabetical order before the other quickfix
        override fun getFamilyName() = "Add label for this command"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            LatexAddLabelToCommandIntention(command.createSmartPointer()).invoke(
                project,
                command.containingFile.openedTextEditor(),
                command.containingFile
            )
        }
    }

    private class InsertLabelInEnvironmentFix : LocalQuickFix {

        override fun getFamilyName() = "Add label for this environment"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexEnvironment
            LatexAddLabelToEnvironmentIntention(command.createSmartPointer()).invoke(
                project,
                command.containingFile.openedTextEditor(),
                command.containingFile
            )
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

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.openapi.util.TextRange
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEndCommand
import nl.hannahsten.texifyidea.psi.LatexNoMathContent
import nl.hannahsten.texifyidea.settings.conventions.TexifyConventionsSettingsManager
import nl.hannahsten.texifyidea.ui.CreateFileDialog
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.writeToFileUndoable
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexTooLargeSectionInspection : TexifyInspectionBase() {

    companion object {

        /**
         * All commands that count as inspected sections in order of hierarchy.
         */
        private val SECTION_NAMES = listOf("\\chapter", "\\section")

        /**
         * Looks up the section command that comes after the given command.
         *
         * The next commands has the same or higher level as the given one,
         * meaning that a \section will stop only  on \section and higher.
         *
         * This was written to require that a chpter or section command be passed
         *
         * As previously written, this would just match the first and second matching sections, but now
         * it will search ahead to find the first equal or bigger section, or EOF, whichever comes first
         */
        fun findNextSection(command: LatexCommands): PsiElement? {
            // Scan all commands.
            var commands = command.containingFile.commandsInFile().toList()
            commands = commands.subList(commands.indexOf(command) + 1, commands.size)

            val indexOfCurrent = SECTION_NAMES.indexOf(command.name)

            for (j in commands.indices) {
                val next = commands[j]

                val indexOfNext = SECTION_NAMES.indexOf(next.name)
                if (indexOfNext in 0..indexOfCurrent) {
                    return commands[j]
                }
            }

            // If no command was found, find the end of the document.
            return command.containingFile.childrenOfType(LatexEndCommand::class)
                .lastOrNull { it.environmentName() == "document" }
        }
    }

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "TooLargeSection"

    override val ignoredSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Too large sections"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.commandsInFile()
            .filter { cmd -> SECTION_NAMES.contains(cmd.name) }

        for (i in commands.indices) {
            if (!isTooLong(commands[i], findNextSection(commands[i]))) {
                continue
            }

            if (isAlreadySplit(commands[i], commands)) {
                return descriptors
            }

            descriptors.add(
                manager.createProblemDescriptor(
                    commands[i],
                    "Section is long and may be moved to a separate file.",
                    InspectionFix(),
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )
        }

        return descriptors
    }

    /**
     * Checks if the given file is already a split up section or chapter, with [command] being the only section/chapter
     * in this file. (If [command] is a \chapter, \section can still occur.)
     */
    private fun isAlreadySplit(command: LatexCommands, commands: Collection<LatexCommands>): Boolean {
        return commands.asSequence().filter { cmd -> cmd.name == command.name }.count() <= 1
    }

    /**
     * Checks if the given command starts a section that is too long.
     *
     * @param command
     *         The section command to start checking from.
     * @param nextCommand
     *         The section command after the `command` one, or `null` when there is no such command.
     * @return `true` when the command starts a section that is too long.
     */
    private fun isTooLong(command: LatexCommands, nextCommand: PsiElement?): Boolean {
        if (!SECTION_NAMES.contains(command.name)) {
            return false
        }

        val file = command.containingFile
        val startIndex = command.textOffset + command.textLength
        val endIndex = nextCommand?.textOffset ?: file.textLength

        val conventionSettings = TexifyConventionsSettingsManager.getInstance(command.project).getSettings()
        val maxSectionSize = conventionSettings.currentScheme.maxSectionSize
        return (endIndex - startIndex) >= maxSectionSize
    }

    /**
     * @author Hannah Schellekens
     */
    class InspectionFix : LocalQuickFix {

        companion object {

            /**
             * Finds the label command of the given command.
             */
            fun findLabel(cmd: LatexCommands): LatexCommands? {
                val nextSibling = cmd.firstParentOfType(LatexNoMathContent::class)
                    ?.nextSiblingIgnoreWhitespace()
                    ?.firstChildOfType(LatexCommands::class) ?: return null
                return if (nextSibling.name == LatexGenericRegularCommand.LABEL.commandWithSlash) nextSibling else null
            }
        }

        override fun startInWriteAction() = false

        override fun getFamilyName() = "Move section to another file"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val cmd = descriptor.psiElement as LatexCommands
            val nextCmd = findNextSection(cmd)
            val label = findLabel(cmd)
            val file = cmd.containingFile
            val document = PsiDocumentManager.getInstance(project).getDocument(file) ?: return

            val startIndex = label?.endOffset() ?: cmd.endOffset()
            val cmdIndent = document.lineIndentation(document.getLineNumber(nextCmd?.textOffset ?: 0))
            val endIndex = (nextCmd?.textOffset ?: document.textLength) - cmdIndent.length
            val text = document.getText(TextRange(startIndex, endIndex)).trimEnd().removeIndents()

            // Create new file.
            val fileNameBraces = if (cmd.parameterList.isNotEmpty()) cmd.parameterList[0].text else return

            // Remove the braces of the LaTeX command before creating a filename of it
            val fileName = fileNameBraces.removeAll("{", "}")
                .formatAsFileName()
            val root = file.findRootFile().containingDirectory?.virtualFile?.canonicalPath ?: return

            // Display a dialog to ask for the location and name of the new file.
            val filePath =
                CreateFileDialog(file.containingDirectory?.virtualFile?.canonicalPath, fileName.formatAsFileName())
                    .newFileFullPath ?: return

            runWriteAction {
                val fn = writeToFileUndoable(project, filePath, text, root)
                document.deleteString(startIndex, endIndex)
                LocalFileSystem.getInstance().refresh(true)
                val indent = cmd.findIndentation()
                document.insertString(startIndex, "\n$indent\\input{${fn.dropLast(4)}}\n\n")
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.codestyle

import nl.hannahsten.texifyidea.file.ClassFileType
import nl.hannahsten.texifyidea.file.StyleFileType
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import java.util.regex.Pattern

class LatexUsePackageInPackageInspection : TexifyRegexInspection(
    inspectionDisplayName = "Use of \\usepackage{...} instead of \\RequirePackage{...}",
    inspectionId = "UsePackageInPackage",
    errorMessage = { "Use \\RequirePackage{...} instead of \\usepackage{...}" },
    // The \\usepackage syntax is \\usepackage[<options>]{<package name>}[<version info>].
    pattern = Pattern.compile("\\\\usepackage(\\[[^]]*])?\\{([^}]*)}(\\[[^]]*])?"),
    quickFixName = { "Replace with \\RequirePackage" },
    replacement = { matcher, _ -> "\\RequirePackage${matcher.group(1) ?: ""}{${matcher.group(2)}}${matcher.group(3) ?: ""}" },
    cancelIf = { _, psiFile ->
        // Cancel if the usepackage was found outside a class or style file.
        psiFile.virtualFile?.extension !in setOf(ClassFileType.defaultExtension, StyleFileType.defaultExtension)
    }
)

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.RequiredArgument
import nl.hannahsten.texifyidea.lang.commands.RequiredFileArgument
import nl.hannahsten.texifyidea.util.files.commandsInFile
import java.io.File

/**
 * @author Thomas
 */
class LatexAbsolutePathInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "AbsolutePath"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.commandsInFile()

        // Loop through commands of file
        for (command in commands) {
            // There may be multiple commands with this name, just guess the first one
            val latexCommand = LatexCommand.lookup(command.name)?.firstOrNull() ?: continue

            // Arguments from the LatexCommand (so the command as hardcoded in e.g. LatexRegularCommand)
            val requiredArguments = latexCommand.arguments.mapNotNull { it as? RequiredArgument }

            // Loop through arguments
            for (i in command.getRequiredParameters().indices) {
                // Find the corresponding requiredArgument
                val requiredArgument = if (i < requiredArguments.size) requiredArguments[i] else requiredArguments.lastOrNull() ?: continue

                // Check if the actual argument is a file argument or continue with the next argument
                val fileArgument = requiredArgument as? RequiredFileArgument ?: continue
                val offset = command.text.indexOf(command.getRequiredParameters()[i])
                if (offset == -1) continue
                val range = TextRange(0, command.getRequiredParameters()[i].length).shiftRight(offset)

                if (File(range.substring(command.text)).isAbsolute && !fileArgument.isAbsolutePathSupported) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            range,
                            "No absolute path allowed here",
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                        )
                    )
                }
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.execution.configuration.EnvironmentVariablesData
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.getBibtexRunConfigurations
import nl.hannahsten.texifyidea.util.getLatexRunConfigurations
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.parser.requiredParameter

/**
 * BIBINPUTS cannot handle paths which start with ../  in the \bibliography command, e.g. \bibliography{../mybib}.
 * Solution: set the BIBINPUTS path to the parent and use \bibliography{mybib} instead (or use a fake subfolder and do \bibliography{fake/../../mybib}
 * See https://tex.stackexchange.com/questions/406024/relative-paths-with-bibinputs
 *
 * [Edit March 2023] I cannot reproduce this problem anymore, maybe it was a bug in bibtex that has been fixed?
 */
class LatexBibinputsRelativePathInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "BibinputsRelativePath"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val commandsWithRelativePath = file.commandsInFile(LatexGenericRegularCommand.BIBLIOGRAPHY.cmd).filter { it.requiredParameter(0)?.startsWith("../") == true }
        if (commandsWithRelativePath.isEmpty()) return emptyList()

        // If not using BIBINPUTS, all is fine and it will work.
        val usesBibinputs = file.getBibtexRunConfigurations()
            .any { config -> config.environmentVariables.envs.keys.any { it == "BIBINPUTS" } }

        if (!usesBibinputs) return emptyList()

        val descriptors = descriptorList()
        commandsWithRelativePath.forEach { command ->
            descriptors.add(
                manager.createProblemDescriptor(
                    command,
                    TextRange(0, command.textLength - 1),
                    "You cannot use both BIBINPUTS and a path that starts with ../ in \\bibliography",
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly,
                    RelativePathFix
                )
            )
        }
        return descriptors
    }

    object RelativePathFix : LocalQuickFix {

        override fun getFamilyName() = "Remove relative part from argument and from BIBINPUTS"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val oldNode = descriptor.psiElement.node
            val newText = oldNode.text.replaceFirst("../", "")
            val newNode = LatexPsiHelper(project).createFromText(newText).firstChild.node ?: return
            descriptor.psiElement.parent.node.replaceChild(oldNode, newNode)

            // Fix BIBINPUTS
            project
                .getLatexRunConfigurations()
                .filter { it.mainFile == descriptor.psiElement.containingFile.findRootFile().virtualFile }
                .flatMap { it.bibRunConfigs }
                .map { it.configuration }
                .filterIsInstance<BibtexRunConfiguration>()
                .forEach { config ->
                    val envs = config.environmentVariables.envs.toMutableMap()
                    val oldPath = envs["BIBINPUTS"] ?: return@forEach
                    val newPath = oldPath.substring(0, oldPath.lastIndexOf('/'))
                    envs["BIBINPUTS"] = newPath
                    config.environmentVariables = EnvironmentVariablesData.create(envs, config.environmentVariables.isPassParentEnvs)
                }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.inDirectEnvironment
import nl.hannahsten.texifyidea.util.parser.isComment
import nl.hannahsten.texifyidea.util.labels.getLabelDefinitionCommands
import nl.hannahsten.texifyidea.util.labels.getLabelReferenceCommands
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author Johannes Berger
 */
class LatexEscapeAmpersandInspection : TexifyRegexInspection(
    inspectionDisplayName = "Unescaped & character",
    inspectionId = "EscapeAmpersand",
    errorMessage = { """Escape character \ expected""" },
    highlight = ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
    pattern = Pattern.compile("""(?<!\\)&"""),
    replacement = { _, _ -> """\&""" },
    quickFixName = { """Change to \&""" }
) {

    override fun checkContext(matcher: Matcher, element: PsiElement): Boolean {
        if (element.isAmpersandAllowed()) return false
        if (element.isComment()) return false
        return checkContext(element)
    }

    private fun PsiElement.isAmpersandAllowed(): Boolean {
        // Do not trigger inside comments.
        if (this.isComment()) return true

        // Do not trigger in environments that use the ampersand as special character.
        if (this.inDirectEnvironment(EnvironmentMagic.getAllTableEnvironments(project))) return true
        if (this.inDirectEnvironment(EnvironmentMagic.alignableEnvironments)) return true

        // Other exceptions
        val command = this.firstParentOfType(LatexCommands::class)?.name
        return command in CommandMagic.urls ||
            command in project.getLabelReferenceCommands() ||
            command in project.getLabelDefinitionCommands()
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.isDefinitionOrRedefinition
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.parentsOfType
import java.util.regex.Pattern

class LatexEscapeHashOutsideCommandInspection : TexifyRegexInspection(
    inspectionDisplayName = "Unescaped # outside of command definition",
    inspectionId = "EscapeHashOutsideCommand",
    pattern = Pattern.compile("""(?<!\\)#"""),
    errorMessage = { "unescaped #" },
    quickFixName = { "escape #" },
    replacement = { _, _ -> """\#""" }
) {

    override fun checkContext(element: PsiElement): Boolean {
        return super.checkContext(element) && element.parentsOfType<LatexCommands>().all { !it.isDefinitionOrRedefinition() } && element.parentOfType(LatexCommands::class)?.name !in CommandMagic.urls
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author Johannes Berger
 */
class LatexEscapeUnderscoreInspection : TexifyRegexInspection(
    inspectionDisplayName = "Unescaped _ character",
    inspectionId = "EscapeUnderscore",
    errorMessage = { """Escape character \ expected""" },
    highlight = ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
    pattern = Pattern.compile("""(?<!\\)_"""),
    replacement = { _, _ -> """\_""" },
    quickFixName = { """Change to \_""" }
) {

    override fun checkContext(matcher: Matcher, element: PsiElement): Boolean {
        if (element.isUnderscoreAllowed()) return false
        return super.checkContext(matcher, element)
    }

    @Suppress("RedundantIf")
    private fun PsiElement.isUnderscoreAllowed(): Boolean {
        if (this.inMathContext()) return true
        if (this.firstParentOfType(LatexNormalText::class) != null) return false
        if (this.firstParentOfType(LatexCommands::class)?.name in commandsDisallowingUnderscore) return false
        return true
    }

    companion object {

        private val commandsDisallowingUnderscore =
            CommandMagic.sectionMarkers + CommandMagic.textStyles + setOf("""\caption""")
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

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 com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.createSmartPointer
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.ui.CreateFileDialog
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.getFileExtension
import nl.hannahsten.texifyidea.util.files.writeToFileUndoable
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.getFileArgumentsReferences
import nl.hannahsten.texifyidea.util.parser.parentsOfType
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexFileNotFoundInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "FileNotFound"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "File not found"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        // Get commands of this file.
        val commands = file.commandsInFile()

        // Loop through commands of file
        for (command in commands) {
            // Don't resolve references in command definitions, as in \cite{#1} the #1 is not a reference
            if (command.parent.parentsOfType(LatexCommands::class).any { it.name in CommandMagic.commandDefinitionsAndRedefinitions }) {
                continue
            }

            val referencesList = command.getFileArgumentsReferences()
            for (reference in referencesList) {
                if (reference.resolve() == null) {
                    createQuickFixes(reference, descriptors, manager, isOntheFly)
                }
            }
        }

        return descriptors
    }

    private fun createQuickFixes(reference: InputFileReference, descriptors: MutableList<ProblemDescriptor>, manager: InspectionManager, isOntheFly: Boolean) {
        val fileName = reference.key
        val extensions = reference.extensions

        // CTAN packages are no targets of the InputFileReference, so we check them here and don't show a warning if a CTAN package is included
        if (extensions.contains("sty")) {
            val ctanPackages = PackageUtils.CTAN_PACKAGE_NAMES.map { it.lowercase(Locale.getDefault()) }
            if (reference.key.lowercase(Locale.getDefault()) in ctanPackages) return
        }

        val fixes = mutableListOf<LocalQuickFix>()

        // Create quick fixes for all extensions
        extensions.forEach {
            fixes.add(CreateNewFileWithDialogQuickFix(fileName, it, reference.element.createSmartPointer()))
        }

        // Find expected extension
        val extension = fileName.getFileExtension().ifEmpty {
            reference.defaultExtension
        }

        descriptors.add(
            manager.createProblemDescriptor(
                reference.element,
                reference.rangeInElement,
                "File '${fileName.appendExtension(extension)}' not found",
                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                isOntheFly,
                *(fixes.toTypedArray())
            )
        )
    }

    /**
     * Create a new file.
     *
     * @param filePath Path relative to the root file parent.
     */
    class CreateNewFileWithDialogQuickFix(private val filePath: String, private val extension: String, private val elementPointer: SmartPsiElementPointer<LatexCommands>) : LocalQuickFix {

        override fun getFamilyName() = "Create file ${filePath.appendExtension(extension)}"

        override fun startInWriteAction() = false

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val cmd = descriptor.psiElement
            val element = elementPointer.element ?: return
            val file = cmd.containingFile ?: return
            val root = file.findRootFile().containingDirectory?.virtualFile?.canonicalPath ?: return

            // Display a dialog to ask for the location and name of the new file.
            // By default, all inclusion paths are relative to the main file
            val newFilePath = CreateFileDialog(root, filePath.replace("$root/", "").formatAsFilePath())
                .newFileFullPath ?: return

            runWriteAction {
                val expandedFilePath = expandCommandsOnce(newFilePath, file.project, file) ?: newFilePath
                var fileNameRelativeToRoot = writeToFileUndoable(project, expandedFilePath, "", root, extension)

                val command = (cmd as? LatexCommands)?.name
                if (command in CommandMagic.illegalExtensions) {
                    CommandMagic.illegalExtensions[command]?.forEach { fileNameRelativeToRoot = fileNameRelativeToRoot.replace(it, "") }
                }

                InputFileReference.handleElementRename(element, fileNameRelativeToRoot, false)
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.util.files.findIncludedFile

/**
 * This inspection only detects inclusion loops involving two files.
 *
 * If ever it is extended to more files, great care should be taken for a good performance.
 */
open class LatexInclusionLoopInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "InclusionLoop"

    override fun getDisplayName() = "Inclusion loops"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        val allIncludeCommands = LatexIncludesIndex.getItems(file.project)

        // Maps every file to all the files it includes.
        val inclusions: MutableMap<PsiFile, MutableSet<PsiFile>> = HashMap()

        // Find all related files.
        for (command in allIncludeCommands) {
            // Find included files
            val declaredIn = command.containingFile

            // Do not use ImportPackage#searchFileByImportPaths, because if we would do that for every command, that would be way too expensive.
            for (referenced in declaredIn.findIncludedFile(command)) {
                inclusions.getOrPut(declaredIn) { mutableSetOf() }.add(referenced)

                if (declaredIn == file && inclusions.getOrDefault(referenced, mutableSetOf()).contains(declaredIn)) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            TextRange(0, command.textLength - 1),
                            "File inclusion loop found for files ${referenced.name} and ${declaredIn.name}.",
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                        )
                    )
                }
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import java.util.regex.Pattern

class LatexLabelBeforeCaptionInspection : TexifyRegexInspection(
    inspectionDisplayName = "Label before caption",
    inspectionId = "LabelBeforeCaption",
    errorMessage = { "A label should come after the caption" },
    // Match label before caption, but not if a label follows the caption, because then the label before the caption
    // probably does not intend to label the caption
    pattern = Pattern.compile("(\\\\label\\{[^}]*})(\\s*)(\\\\caption\\{[^}]*})(?!\\s*\\\\label)"),
    quickFixName = { "Swap label and caption" },
    replacement = { it, _ -> it.group(3) + it.group(2) + it.group(1) }
)

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

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.PsiDocumentManager
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexMissingDocumentEnvironmentInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "MissingDocumentEnvironment"

    override val ignoredSuppressionScopes = EnumSet.of(
        MagicCommentScope.ENVIRONMENT,
        MagicCommentScope.MATH_ENVIRONMENT,
        MagicCommentScope.COMMAND,
        MagicCommentScope.GROUP
    )!!

    override fun getDisplayName() = "Missing document environment"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        // Workaround, because .sty files also return LatexFileType.INSTANCE
        if (file.virtualFile.extension != LatexFileType.defaultExtension) {
            return descriptors
        }

        for (referencedFile in file.referencedFileSet()) {
            val beginCommands = referencedFile.childrenOfType(LatexBeginCommand::class)
            if (beginCommands.any { it.text == "\\begin{document}" }) {
                return descriptors
            }
        }

        descriptors.add(
            manager.createProblemDescriptor(
                file,
                "Document doesn't contain a document environment.",
                InspectionFix(),
                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                isOntheFly
            )
        )

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    private class InspectionFix : LocalQuickFix {

        override fun getFamilyName() = "Add a document environment"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val psiElement = descriptor.psiElement
            val file = psiElement.containingFile
            val document = PsiDocumentManager.getInstance(project).getDocument(file)

            document?.insertString(
                document.textLength,
                """
                |
                |\begin{document}
                |
                |\end{document}
                """.trimMargin()
            )
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

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.PsiDocumentManager
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexMissingDocumentclassInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "MissingDocumentclass"

    override val ignoredSuppressionScopes = EnumSet.of(
        MagicCommentScope.ENVIRONMENT,
        MagicCommentScope.MATH_ENVIRONMENT,
        MagicCommentScope.COMMAND,
        MagicCommentScope.GROUP
    )!!

    override fun getDisplayName() = "Missing documentclass"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        // Workaround, because .sty files also return LatexFileType.INSTANCE
        if (file.virtualFile.extension != LatexFileType.defaultExtension) {
            return descriptors
        }

        val hasDocumentclass = file.commandsInFileSet().asSequence()
            .filter { cmd -> cmd.name == "\\documentclass" }
            .count() > 0

        if (!hasDocumentclass) {
            descriptors.add(
                manager.createProblemDescriptor(
                    file,
                    "Document doesn't contain a \\documentclass command.",
                    InspectionFix(),
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )
        }

        return descriptors
    }

    private class InspectionFix : LocalQuickFix {

        override fun getFamilyName() = "Add \\documentclass{article}"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val psiElement = descriptor.psiElement
            val file = psiElement.containingFile
            val document = PsiDocumentManager.getInstance(project).getDocument(file) ?: return

            document.insertString(0, "\\documentclass{article}\n")
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.util.files.commandsInFile

/**
 * @author Lukas Heiligenbrunner
 */
class LatexMultipleGraphicsPathInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "MultipleGraphicsPath"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.commandsInFile()

        // Check if a graphicspath is defined
        val paths = commands.filter { it.name == "\\graphicspath" }

        // Throw error on multiple definition of \graphicspath.
        if (paths.size > 1) {
            for (i in paths) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        i,
                        TextRange(0, i.text.length),
                        "\\graphicspath is already used elsewhere",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        RemoveFix()
                    )
                )
            }
        }

        return descriptors
    }

    /**
     * Remove the command line.
     */
    class RemoveFix : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Remove this Line"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            descriptor.psiElement.delete()
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.findFile
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.replaceString
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import java.util.*

/**
 * @author Sten Wessel
 */
open class LatexNestedIncludesInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "NestedIncludes"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Nested includes"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()
        val root = file.findRootFile()

        val isInclude = LatexIncludesIndex.getItemsInFileSet(file).any {
            it.name == "\\include" && it.requiredParameter(0)?.let { f -> root.findFile(f) } == file
        }

        if (!isInclude) {
            return descriptors
        }

        file.commandsInFile().asSequence()
            .filter { it.name == "\\include" }
            .forEach {
                descriptors.add(
                    manager.createProblemDescriptor(
                        it,
                        TextRange.allOf(it.text),
                        "Includes cannot be nested",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        ConvertToInputFix
                    )
                )
            }

        return descriptors
    }

    /**
     * @author Sten Wessel
     */
    object ConvertToInputFix : LocalQuickFix {

        override fun getFamilyName() = "Convert to \\input"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val document = command.containingFile.document() ?: return

            val fileName = command.requiredParameter(0) ?: return

            document.replaceString(command.textRange, "\\input{$fileName}")
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.replaceString
import java.util.*

/**
 * @author Sten Wessel
 */
open class LatexNoExtensionInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "NoExtension"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "File argument should not include the extension"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        file.commandsInFile().asSequence()
            .filter { it.name in CommandMagic.illegalExtensions }
            .filter { command ->
                CommandMagic.illegalExtensions[command.name]!!.any {
                        extension ->
                    command.getRequiredParameters().any { it.split(",").any { parameter -> parameter.endsWith(extension) } }
                }
            }
            .forEach { command ->
                val parameterList = command.getRequiredParameters().map { it.split(",") }.flatten()
                var offset = command.parameterList.first { it.requiredParam != null }.textOffset - command.textOffset + 1
                for (parameter in parameterList) {
                    if (CommandMagic.illegalExtensions[command.name]!!.any { parameter.endsWith(it) }) {
                        descriptors.add(
                            manager.createProblemDescriptor(
                                command,
                                TextRange(offset, offset + parameter.length),
                                "File argument should not include the extension",
                                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                                isOntheFly,
                                RemoveExtensionFix
                            )
                        )
                    }

                    // Assume all parameter are comma separated
                    offset += parameter.length + ",".length
                }
            }

        return descriptors
    }

    /**
     * @author Sten Wessel
     */
    object RemoveExtensionFix : LocalQuickFix {

        override fun getFamilyName() = "Remove file extension from parameters"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val document = command.containingFile.document() ?: return

            val parameterList = command.getRequiredParameters().map { it.split(",") }.flatten()
            var offset = 0
            for (parameter in parameterList) {
                if (CommandMagic.illegalExtensions.getOrDefault(command.name, null)?.any { parameter.endsWith(it) } == true) {
                    val range = TextRange(offset, offset + parameter.length).shiftRight(command.parameterList.first { it.requiredParam != null }.textOffset + 1)
                    val replacement = CommandMagic.illegalExtensions[command.name]
                        ?.find { parameter.endsWith(it) }
                        ?.run { parameter.removeSuffix(this) } ?: break
                    document.replaceString(range, replacement)

                    // Maintain offset for any removed part
                    offset -= (range.length - replacement.length)
                }

                // Assume all parameter are comma separated
                offset += parameter.length + ",".length
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

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.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexEndCommand
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexNonMatchingEnvironmentInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "NonMatchingEnvironment"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Non matching environment commands"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val begins = file.childrenOfType(LatexBeginCommand::class)
        for (begin in begins) {
            val end = begin.endCommand() ?: continue
            val beginEnvironment = begin.environmentName() ?: continue
            val endEnvironment = end.environmentName() ?: continue
            if (beginEnvironment == endEnvironment) {
                continue
            }

            // Add descriptor to begin.
            descriptors.add(
                manager.createProblemDescriptor(
                    begin,
                    "DefaultEnvironment name does not match with the name in \\end.",
                    MatchBeginFix(beginEnvironment),
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )

            // Add descriptor to end.
            descriptors.add(
                manager.createProblemDescriptor(
                    end,
                    "DefaultEnvironment name does not match with the name in \\begin.",
                    MatchEndFix(endEnvironment),
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )
        }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    private open class MatchBeginFix(val environmentName: String) : LocalQuickFix {

        override fun getFamilyName() = "Change \\end environment to '$environmentName'"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexBeginCommand
            val file = command.containingFile
            val document = file.document() ?: return
            val end = command.endCommand() ?: return
            val environment = command.environmentName()

            document.replaceString(end.textOffset, end.endOffset(), "\\end{$environment}")
        }
    }

    /**
     * @author Hannah Schellekens
     */
    private open class MatchEndFix(val environmentName: String) : LocalQuickFix {

        override fun getFamilyName() = "Change \\begin environment to '$environmentName'"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexEndCommand
            val file = command.containingFile
            val document = file.document() ?: return
            val begin = command.beginCommand() ?: return
            val environment = command.environmentName()

            document.replaceString(begin.textOffset, begin.endOffset(), "\\begin{$environment}")
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.commands.LatexNewDefinitionCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.GeneralMagic
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.matches
import nl.hannahsten.texifyidea.util.parser.previousCommand
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexNonMatchingIfInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override fun getDisplayName() = "Open if-then-else control sequence"

    override val inspectionId = "NonMatchingIf"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        // Find matches.
        val stack = ArrayDeque<LatexCommands>()
        val commands = file.commandsInFile().sortedBy { it.textOffset }
        for (command in commands) {
            val name = command.name
            if (command.name in CommandMagic.endIfs) {
                // Non-opened fi.
                if (stack.isEmpty()) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            "No matching \\if-command found",
                            GeneralMagic.noQuickFix,
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                        )
                    )
                    continue
                }

                stack.pop()
            }
            else if (PatternMagic.ifCommand.matches(name) && name !in CommandMagic.ignoredIfs && command.previousCommand()?.name != LatexNewDefinitionCommand.NEWIF.commandWithSlash) {
                stack.push(command)
            }
        }

        // Mark unclosed ifs.
        for (cmd in stack) {
            descriptors.add(
                manager.createProblemDescriptor(
                    cmd,
                    "If statement should probably be closed with \\fi",
                    GeneralMagic.noQuickFix,
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.appendExtension
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.replaceString
import java.util.*

/**
 * See [LatexNoExtensionInspection].
 */
open class LatexRequiredExtensionInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "RequiredExtension"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "File argument should include the extension"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        file.commandsInFile().asSequence()
            .filter { it.name in CommandMagic.requiredExtensions }
            .filter { command ->
                CommandMagic.requiredExtensions[command.name]!!.any {
                        extension ->
                    command.getRequiredParameters().any { !it.split(",").any { parameter -> parameter.endsWith(extension) } }
                }
            }
            .forEach { command ->
                val parameterList = command.getRequiredParameters().map { it.split(",") }.flatten()
                var offset = 0
                for (parameter in parameterList) {
                    if (CommandMagic.requiredExtensions[command.name]!!.any { !parameter.endsWith(it) }) {
                        descriptors.add(
                            manager.createProblemDescriptor(
                                command,
                                TextRange(offset, offset + parameter.length).shiftRight(command.commandToken.textLength + 1),
                                "File argument should include the extension",
                                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                                isOntheFly,
                                AddExtensionFix
                            )
                        )
                    }

                    // Assume all parameter are comma separated
                    offset += parameter.length + ",".length
                }
            }

        return descriptors
    }

    /**
     * See [LatexNoExtensionInspection.RemoveExtensionFix].
     */
    object AddExtensionFix : LocalQuickFix {

        override fun getFamilyName() = "Add file extension for parameters"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val document = command.containingFile.document() ?: return

            val parameterList = command.getRequiredParameters().map { it.split(",") }.flatten()
            var offset = 0
            for (parameter in parameterList) {
                if (CommandMagic.requiredExtensions[command.name]!!.any { !parameter.endsWith(it) }) {
                    val range = TextRange(offset, offset + parameter.length).shiftRight(command.parameterList.first { it.requiredParam != null }.textOffset + 1)
                    val replacement = CommandMagic.requiredExtensions[command.name]
                        ?.find { !parameter.endsWith(it) }
                        ?.run { parameter.appendExtension(this) } ?: break
                    document.replaceString(range, replacement)

                    // Maintain offset for any added part
                    offset -= (range.length - replacement.length)
                }

                // Assume all parameter are comma separated
                offset += parameter.length + ",".length
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview
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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import com.intellij.webSymbols.references.WebSymbolReferenceProvider.Companion.startOffsetIn
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.psi.LatexRequiredParam
import nl.hannahsten.texifyidea.util.containsAny
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.parser.requiredParameter

open class LatexSuspiciousSectionFormattingInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    val formatting = setOf("~", "\\\\")

    override fun getDisplayName() = "Suspicious formatting in the required argument of a sectioning command"

    override val inspectionId = "SuspiciousSectionFormatting"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        return file.commandsInFile()
            .asSequence()
            .filter { it.name in CommandMagic.sectionMarkers }
            .filter { it.parameterList.mapNotNull { param -> param.optionalParam }.isEmpty() }
            .filter { it.requiredParameter(0)?.containsAny(formatting) == true }
            .map { psiElement ->
                val requiredParam = psiElement.firstChildOfType(LatexRequiredParam::class)
                // Plus 1 for the opening brace.
                val startOffset = requiredParam?.startOffsetIn(psiElement)?.plus(1) ?: 0
                // Minus 2 for the braces surrounding the parameter.
                val endOffset = requiredParam?.textLength?.minus(2)?.plus(startOffset) ?: psiElement.textLength
                manager.createProblemDescriptor(
                    psiElement,
                    TextRange(startOffset, endOffset),
                    "Suspicious formatting in ${psiElement.name}",
                    ProblemHighlightType.WARNING,
                    isOntheFly,
                    AddOptionalArgumentQuickFix(formatting)
                )
            }
            .toList()
    }

    class AddOptionalArgumentQuickFix(@SafeFieldForPreview val formatting: Set<String>) : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Fix formatting in table of contents and running head"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val requiredParamText = command.requiredParameter(0)
            val optionalParamText = requiredParamText?.replace(Regex(formatting.joinToString("", prefix = "[", postfix = "]")), " ")
                ?: return
            val optionalArgument = LatexPsiHelper(project).createOptionalParameter(optionalParamText) ?: return

            command.addAfter(optionalArgument, command.commandToken)
            // Create a new command and completely replace the old command so all the psi methods will recompute instead
            // of using old values from their cache.
            val newCommand = LatexPsiHelper(project).createFromText(command.text).firstChildOfType(LatexCommands::class)
                ?: return
            command.replace(newCommand)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInsight.hint.HintManager
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.openapi.application.runInEdt
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.inspections.latex.probablebugs.LatexUnicodeInspection.EscapeUnicodeFix
import nl.hannahsten.texifyidea.inspections.latex.probablebugs.LatexUnicodeInspection.InsertUnicodePackageFix
import nl.hannahsten.texifyidea.lang.Diacritic
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.commands.LatexMathCommand
import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand
import nl.hannahsten.texifyidea.psi.LatexMathEnvironment
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import nl.hannahsten.texifyidea.settings.sdk.MiktexWindowsSdk
import nl.hannahsten.texifyidea.settings.sdk.TexliveSdk
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.insertUsepackage
import nl.hannahsten.texifyidea.util.magic.PackageMagic
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.selectedRunConfig
import org.apache.maven.artifact.versioning.DefaultArtifactVersion
import org.jetbrains.annotations.Nls
import java.text.Normalizer
import java.util.regex.Pattern

/**
 * Checks whether Unicode is enabled in the file and flags illegal characters when they are not
 * supported.
 *
 *
 * Flags non-ASCII characters outside of math mode only when Unicode support packages are not
 * loaded. Unicode support is assumed when the packages `inputenc` and `fontenc` are
 * loaded. The inspection always flags non-ASCII characters in math mode, because Unicode math has
 * no support package in pdfLaTeX.
 *
 * Quick fixes:
 * * Escape the character: see [EscapeUnicodeFix]
 * * (When outside math mode) Insert Unicode support packages: see [InsertUnicodePackageFix]
 *
 * @author Sten Wessel
 */
class LatexUnicodeInspection : TexifyInspectionBase() {

    companion object {

        private val BASE_PATTERN = Pattern.compile("^\\p{ASCII}*")

        /**
         * Checks whether Unicode support is enabled for the file.
         *
         * Support is assumed when the packages `inputenc` and `fontenc` are
         * loaded, or when a compiler is used which does not need these packages. The
         * loaded options are not checked.
         *
         * @param file The file to check support for.
         * @return Whether Unicode support is enabled.
         */
        internal fun unicodeEnabled(file: PsiFile): Boolean {
            // TeX Live 2018 is UTF-8 by default and loads inputenc automatically
            val compilerCompat = file.project.selectedRunConfig()?.compiler
            if (compilerCompat == LatexCompiler.LUALATEX ||
                compilerCompat == LatexCompiler.XELATEX ||
                TexliveSdk.version >= 2018 ||
                MiktexWindowsSdk().getVersion(null) >= DefaultArtifactVersion("2.9.7350")
            ) {
                return true
            }

            // If we can't figure it out by compiler, check included packages
            val included = file.includedPackages()
            return PackageMagic.unicode.stream().allMatch { p -> included.contains(p) }
        }
    }

    override val inspectionGroup = InsightGroup.LATEX

    @Nls
    override fun getDisplayName(): String {
        return "Unsupported non-ASCII character"
    }

    override val inspectionId = "Unicode"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val hasUnicode = unicodeEnabled(file)

        val descriptors = descriptorList()

        val texts = PsiTreeUtil.findChildrenOfType(file, LatexNormalText::class.java)
        for (text in texts) {
            val matcher = PatternMagic.nonAscii.matcher(text.text)
            while (matcher.find()) {
                val inMathMode = PsiTreeUtil.getParentOfType(text, LatexMathEnvironment::class.java) != null

                if (!inMathMode && hasUnicode) {
                    // Unicode is supported, characters are legal
                    continue
                }

                val fix = if (inMathMode) {
                    null
                }
                else {
                    InsertUnicodePackageFix()
                }
                val fixes = listOfNotNull(fix).toTypedArray()

                descriptors.add(
                    manager.createProblemDescriptor(
                        text,
                        TextRange(matcher.start(), matcher.end()),
                        "Unsupported non-ASCII character",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        EscapeUnicodeFix(inMathMode),
                        *fixes
                    )
                )
            }
        }

        return descriptors
    }

    /**
     * Inserts required packages for Unicode support.
     *
     *
     * This is only available for Unicode support outside math mode, since Unicode in math is not
     * available in pdfLaTeX.
     *
     *
     * This fix loads the packages `inputenc` with option `utf8` and `fontenc`
     * with option `T1` when needed to enable unicode support.
     */
    private class InsertUnicodePackageFix : LocalQuickFix {

        @Nls
        override fun getFamilyName(): String {
            return "Include Unicode support packages"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val file = descriptor.psiElement.containingFile

            PackageMagic.unicode.forEach { p ->
                file.insertUsepackage(p)
            }
        }
    }

    /**
     * Attempts to escape the non-ASCII character to avoid encoding issues.
     *
     * The following attempts are made, in order, to determine a suitable replacement:   1.  The
     * character is matched against the *display* attribute of either [ ] or [LatexCommand] (where appropriate).
     * When there is a match, the corresponding command is used as replacement. 1.  The character is decomposed to
     * separate combining marks (see also [Unicode](http://unicode.org/reports/tr15/)).
     * An attempt is made to match the combining sequence against LaTeX character diacritical
     * commands. See [Diacritic] for a list of supported diacritics for both non-math and math
     * mode. When there is a match for all combining marks, the sequence of LaTeX commands is used
     * as replacement. Also, when the letters *i* or *j* are used in combination with
     * a diacritic their dotless versions are substituted.
     *
     * When neither of these steps is successful, the character is too exotic to replace and an
     * appropriate fail message is shown.
     */
    private class EscapeUnicodeFix(private val inMathMode: Boolean) : LocalQuickFix {

        @Nls
        override fun getFamilyName(): String {
            return "Escape Unicode character"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val element = descriptor.psiElement
            val editor = FileEditorManager.getInstance(project).selectedTextEditor
            val replacement = getReplacementFromProblemDescriptor(descriptor)

            // When no replacement is found, show error message
            val document = PsiDocumentManager.getInstance(project).getDocument(element.containingFile)
            if (replacement == null) {
                if (editor != null) {
                    runInEdt {
                        HintManager.getInstance().showErrorHint(editor, "Character could not be converted")
                    }
                }
                return
            }

            // Fill in replacement
            // To improve this, this should be done by replacing psi elements using LatexPsiHelper
            val range = descriptor.textRangeInElement.shiftRight(element.textOffset)
            document?.replaceString(range.startOffset, range.endOffset, replacement)
        }

        /**
         * Extend the heuristics implemented in [LocalQuickFix.generatePreview] that predict that the fix can not be applied.
         * We cannot use the automatically generated preview because in the applyFix we sometimes show a UI element, which breaks the preview
         */
        override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo {
            val replacement = getReplacementFromProblemDescriptor(previewDescriptor) ?: return IntentionPreviewInfo.EMPTY
            val element = previewDescriptor.psiElement
            // We don't directly work on the document, as in a preview that is not available
            val origText = element.text
            val range = previewDescriptor.textRangeInElement
            val modifiedText = origText.replaceRange(range.startOffset, range.endOffset, replacement)

            return IntentionPreviewInfo.CustomDiff(LatexFileType, element.containingFile.name, origText, modifiedText)
        }

        private fun getReplacementFromProblemDescriptor(descriptor: ProblemDescriptor): String? {
            val element = descriptor.psiElement

            val c = try {
                descriptor.textRangeInElement.substring(element.text)
            }
            catch (e: IndexOutOfBoundsException) {
                return null
            }

            // Try to find in lookup for special command
            val replacement: String?
            val command: LatexCommand? = if (inMathMode) {
                LatexMathCommand.findByDisplay(c)?.firstOrNull()
            }
            else {
                LatexRegularCommand.findByDisplay(c)?.firstOrNull()
            }

            // Replace with found command or with standard substitution
            replacement = if (command != null) {
                "\\" + command.command
            }
            else {
                findReplacement(c)
            }
            return replacement
        }

        private fun findReplacement(c: String): String? {
            val n = Normalizer.normalize(c, Normalizer.Form.NFD)

            // Extract base characters
            val matcher = BASE_PATTERN.matcher(n)
            matcher.find()
            val base = matcher.group()

            // Extract modifiers
            val mods = n.substring(matcher.end()).split("".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()

            val diacritics = (mods.indices)
                // Modifiers in reversed order
                .map { mods[mods.size - 1 - it] }
                .mapNotNull {
                    @Suppress("USELESS_CAST")
                    if (inMathMode)
                        Diacritic.Math.fromUnicode(it) as? Diacritic
                    else
                        Diacritic.Normal.fromUnicode(it) as? Diacritic
                }

            return Diacritic.buildChain(base, diacritics)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.labels.findLatexAndBibtexLabelStringsInFileSet
import nl.hannahsten.texifyidea.util.parser.firstParentOfType
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import java.lang.Integer.max
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexUnresolvedReferenceInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "UnresolvedReference"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.COMMAND, MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Unresolved reference"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val labels = file.findLatexAndBibtexLabelStringsInFileSet()
        val commands = file.commandsInFile()

        for (command in commands) {
            if (!CommandMagic.reference.contains(command.name)) {
                continue
            }

            // Don't resolve references in command definitions, as in \cite{#1} the #1 is not a reference
            if (command.parent.firstParentOfType(LatexCommands::class)?.name in CommandMagic.commandDefinitionsAndRedefinitions) {
                continue
            }

            val required = command.getRequiredParameters()
            if (required.isEmpty()) {
                continue
            }

            val parts = required[0].split(",")
            for (i in parts.indices) {
                val part = parts[i]
                if (part == "*") continue

                // The cleveref package allows empty items to customize enumerations
                if (part.isEmpty() && (command.commandToken.text == "\\cref" || command.commandToken.text == "\\Cref")) continue

                // If there is no label with this required label parameter value
                if (!labels.contains(part.trim())) {
                    // We have to subtract from the total length, because we do not know whether optional
                    // parameters were included with [a][b][c] or [a,b,c] in which case the
                    // indices of the parts are different with respect to the start of the command
                    var offset = command.textLength - parts.sumOf { it.length + 1 }
                    for (j in 0 until i) {
                        offset += parts[j].length + 1
                    }

                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            TextRange.from(max(offset, 0), part.length),
                            "Unresolved reference '$part'",
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                        )
                    )
                }
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex.redundancy

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.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.forcedFirstRequiredParameterAsCommand
import nl.hannahsten.texifyidea.util.parser.isKnown
import nl.hannahsten.texifyidea.util.replaceString
import java.util.*

/**
 * Warns for commands that (maybe by mistake) redefine commands that are defined by LaTeX (packages).
 *
 * @author Sten Wessel
 */
class LatexCommandAlreadyDefinedInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "CommandAlreadyDefined"

    override val ignoredSuppressionScopes = EnumSet.of(
        MagicCommentScope.ENVIRONMENT,
        MagicCommentScope.MATH_ENVIRONMENT,
        MagicCommentScope.COMMAND,
        MagicCommentScope.GROUP
    )!!

    override fun getDisplayName() = "Command is already defined"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.commandsInFile()

        for (command in commands) {
            // Error when \newcommand is used on existing command
            if ("\\newcommand" == command.name) {
                val newCommand = command.forcedFirstRequiredParameterAsCommand() ?: continue

                if (newCommand.isKnown()) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            "Command may already be defined in a LaTeX package",
                            true,
                            // Warning because may not be true: command may be defined in a LaTeX package which is not actually imported
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly, RenewCommandFix
                        )
                    )
                }
            }
            // Warning when a builtin command gets overridden
            else if ("\\def" == command.name || "\\let" == command.name) {
                val newCommand = command.forcedFirstRequiredParameterAsCommand() ?: continue

                if (newCommand.isKnown()) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            "Command may already be defined in a LaTeX package",
                            true,
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                        )
                    )
                }
            }
        }

        return descriptors
    }

    object RenewCommandFix : LocalQuickFix {

        override fun getFamilyName() = "Convert to \\renewcommand"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val element = descriptor.psiElement as? LatexCommands ?: return

            val range = element.commandToken.textRange

            val document = element.containingFile.document() ?: return

            document.replaceString(range, "\\renewcommand")
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.redundancy

import com.google.common.collect.HashMultiset
import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.util.parser.definedCommandName
import nl.hannahsten.texifyidea.util.files.definitions
import nl.hannahsten.texifyidea.util.files.definitionsInFileSet
import nl.hannahsten.texifyidea.util.magic.CommandMagic

/**
 * Warns for commands that are defined twice in the same fileset.
 *
 * @author Hannah Schellekens
 */
open class LatexDuplicateDefinitionInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "DuplicateDefinition"

    override fun getDisplayName() = "Duplicate command definitions"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        // Find all defined commands.
        val defined = HashMultiset.create<String>()
        val definitions = file.definitionsInFileSet().filter { it.name in CommandMagic.regularStrictCommandDefinitions }
        for (command in definitions) {
            val name = command.definedCommandName() ?: continue
            defined.add(name)
        }

        // Go monkeys.
        file.definitions()
            .forEach {
                val definedCmd = it.definedCommandName() ?: return@forEach
                if (defined.count(definedCmd) > 1) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            it,
                            "Command '$definedCmd' is defined multiple times",
                            true,
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                        )
                    )
                }
            }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex.redundancy

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.alias.CommandManager
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexParameter
import nl.hannahsten.texifyidea.psi.getEnvironmentName
import nl.hannahsten.texifyidea.util.labels.findBibitemCommands
import nl.hannahsten.texifyidea.util.labels.findLatexLabelingElementsInFileSet
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.EnvironmentMagic
import nl.hannahsten.texifyidea.util.parser.isDefinitionOrRedefinition
import nl.hannahsten.texifyidea.util.parser.parentOfType
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import java.lang.Integer.max
import java.util.*

/**
 * @author Hannah Schellekens, Sten Wessel
 */
open class LatexDuplicateLabelInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId: String = "DuplicateLabel"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Duplicate labels"

    /**
     * checks if any label is used more than once
     */
    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val duplicateLabels =
            getProblemDescriptors(file.findLatexLabelingElementsInFileSet(), isOntheFly, manager, file) {
                when (this) {
                    is LatexCommands -> {
                        val name = this.name ?: return@getProblemDescriptors null
                        if (CommandMagic.labelAsParameter.contains(this.name)) {
                            return@getProblemDescriptors getParameterLabelDescriptor(this)
                        }
                        else {
                            val position =
                                CommandManager.labelAliasesInfo.getOrDefault(name, null)?.positions?.firstOrNull()
                                    ?: return@getProblemDescriptors null
                            return@getProblemDescriptors getCommandLabelDescriptor(this, position)
                        }
                    }
                    is LatexEnvironment -> {
                        if (EnvironmentMagic.labelAsParameter.contains(this.getEnvironmentName())) {
                            return@getProblemDescriptors getParameterLabelDescriptor(this)
                        }

                        return@getProblemDescriptors null
                    }
                    else -> return@getProblemDescriptors null
                }
            }

        val duplicateBibitems = getProblemDescriptors(file.findBibitemCommands(), isOntheFly, manager, file) {
            getCommandLabelDescriptor(this as LatexCommands, 0)
        }

        return duplicateLabels + duplicateBibitems
    }

    /**
     * Creates a LabelDescriptor for an environment that defines a label with an optional parameter
     */
    private fun getParameterLabelDescriptor(env: LatexEnvironment): LabelDescriptor? {
        val label =
            env.beginCommand.getOptionalParameterMap().entries.firstOrNull { e -> e.key.toString() == "label" }?.value
                ?: return null
        val labelString = label.toString()
        return LabelDescriptor(env, labelString, TextRange.from(label.startOffset - env.startOffset, label.textLength))
    }

    /**
     * Creates a LabelDescriptor for a command that defines a label with an optional parameter
     */
    private fun getParameterLabelDescriptor(cmd: LatexCommands): LabelDescriptor? {
        val label =
            cmd.getOptionalParameterMap().entries.firstOrNull { e -> e.key.toString() == "label" }?.value ?: return null
        val labelString = label.toString()
        return LabelDescriptor(cmd, labelString, TextRange.from(label.startOffset - cmd.startOffset, label.textLength))
    }

    /**
     * calculates the offset between commandToken and the parameter defining the label
     */
    private fun skippedParametersLength(parameters: List<LatexParameter>, searched: String): Int {
        val parameterStrings = parameters.map { latexParameter ->
            latexParameter.text
        }
        val toIgnore = parameterStrings.indexOf("{$searched}")
        return parameterStrings.subList(0, max(0, toIgnore)).sumOf { it.length }
    }

    /**
     * Creates a LabelDescriptor for a command that defines a label with a required parameter at the specified position
     */
    private fun getCommandLabelDescriptor(cmd: LatexCommands, position: Int): LabelDescriptor? {
        val label = cmd.requiredParameter(position) ?: return null
        val offset = cmd.commandToken.textLength + skippedParametersLength(cmd.parameterList, label) + 1
        return LabelDescriptor(cmd, label, TextRange.from(offset, label.length))
    }

    data class LabelDescriptor(val element: PsiElement, val label: String, val textRange: TextRange)

    /**
     * @param getLabelDescriptor Get the label string given the command in which it is defined.
     */
    private fun getProblemDescriptors(
        commands: Sequence<PsiElement>, isOntheFly: Boolean, manager: InspectionManager, file: PsiFile,
        getLabelDescriptor: PsiElement.() -> LabelDescriptor?
    ): List<ProblemDescriptor> = commands.toSet() // We don't want duplicate psi elements
        .mapNotNull { command ->
            // When the label is defined in a command definition ignore it, because there could be more than one with #1 as parameter
            if (command.parentOfType(LatexCommands::class).isDefinitionOrRedefinition()) return@mapNotNull null
            command.getLabelDescriptor()
        }
        .groupBy { it.label }
        .values
        .filter { it.size > 1 }
        .flatMap { descriptors ->
            // We can only mark labels in the current file.
            descriptors.filter { it.element.containingFile == file }
                .map { createProblemDescriptor(it, isOntheFly, manager) }
        }

    /**
     * make the mapping from command etc. to ProblemDescriptor
     */
    private fun createProblemDescriptor(desc: LabelDescriptor, isOntheFly: Boolean, manager: InspectionManager):
        ProblemDescriptor {
        return manager.createProblemDescriptor(
            desc.element,
            desc.textRange,
            "Duplicate label '${desc.label}'",
            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
            isOntheFly
        )
    }
}

package nl.hannahsten.texifyidea.inspections.latex.redundancy

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexRequiredParam
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.magic.cmd
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import java.util.*
import kotlin.collections.HashSet

/**
 * @author Hannah Schellekens
 */
open class LatexMultipleIncludesInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "MultipleIncludes"

    override val ignoredSuppressionScopes = EnumSet.of(MagicCommentScope.ENVIRONMENT, MagicCommentScope.MATH_ENVIRONMENT)!!

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Package has been imported multiple times"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        // Find all duplicates.
        val packages = file.includedPackages(onlyDirectInclusions = true).map { it.name }
        val covered = HashSet<String>()
        val duplicates = HashSet<String>()
        packages.filterNotTo(duplicates) {
            covered.add(it)
        }

        // Duplicates!
        file.commandsInFile().asSequence()
            .filter { it.name == LatexGenericRegularCommand.USEPACKAGE.cmd && it.requiredParameter(0) in duplicates }
            .forEach {
                val parameter = it.firstChildOfType(LatexRequiredParam::class) ?: error("There must be a required parameter.")
                descriptors.add(
                    manager.createProblemDescriptor(
                        it,
                        TextRange.from(parameter.textOffset + 1 - it.textOffset, parameter.textLength - 2),
                        "Package has already been included",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex.redundancy

import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.inspections.latex.probablebugs.LatexUnicodeInspection
import nl.hannahsten.texifyidea.lang.Diacritic
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexMathEnvironment
import nl.hannahsten.texifyidea.psi.LatexNoMathContent
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.parser.allCommands
import java.text.Normalizer
import java.util.*

/**
 * @author Sten Wessel
 */
open class LatexRedundantEscapeInspection : TexifyInspectionBase() {

    companion object {

        fun getNormalTextSibling(command: LatexCommands): LatexNormalText? {
            val content = PsiTreeUtil.getParentOfType(command, LatexNoMathContent::class.java)
            val siblingContent = PsiTreeUtil.getNextSiblingOfType(content, LatexNoMathContent::class.java)
            return siblingContent?.normalText
        }
    }

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "RedundantEscape"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.COMMAND)!!

    override fun getDisplayName() = "Redundant escape when Unicode is enabled"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()
        if (!LatexUnicodeInspection.unicodeEnabled(file)) {
            return descriptors
        }

        val commands = file.allCommands()
        for (command in commands) {
            val inMathMode = PsiTreeUtil.getParentOfType(command, LatexMathEnvironment::class.java) != null
            if (inMathMode) {
                // Only works outside of math
                continue
            }

            val diacritic = Diacritic.Normal.fromCommand(command.commandToken.text) ?: continue
            if (diacritic.isTypeable && (command.getRequiredParameters().isNotEmpty() || getNormalTextSibling(command) != null)) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        command,
                        "Redundant diacritic escape",
                        RemoveEscapeFix(),
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }
        }

        return descriptors
    }

    /**
     * @author Sten Wessel
     */
    private class RemoveEscapeFix : LocalQuickFix {

        override fun getFamilyName() = "Replace escape with Unicode character"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val file = command.containingFile
            val document = PsiDocumentManager.getInstance(project).getDocument(file) ?: return

            // Get diacritic information
            val diacritic = Diacritic.Normal.fromCommand(command.commandToken.text) ?: return

            val range: TextRange
            val base: String
            val param = command.getRequiredParameters().getOrNull(0)
            if (param != null) {
                // Just a required parameter
                range = command.textRange
                base = if (param == "\\i" || param == "\\j") param.substring(1) else param
            }
            else {
                // Now find a sibling
                val siblingText = getNormalTextSibling(command)

                if (siblingText != null) {
                    base = siblingText.text
                    range = command.textRange.union(siblingText.textRange)
                }
                else {
                    base = ""
                    range = TextRange.EMPTY_RANGE
                }
            }

            if (base.isEmpty()) {
                val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return
                HintManager.getInstance().showErrorHint(editor, "Cannot insert replacement because the base character is missing")
                return
            }

            // Replace unicode char
            var replacement = base[0] + diacritic.unicode
            if (base.length > 1) {
                replacement += base.substring(1)
            }
            replacement = Normalizer.normalize(replacement, Normalizer.Form.NFC)

            document.replaceString(range.startOffset, range.endOffset, replacement)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.redundancy

import com.intellij.psi.PsiElement
import com.intellij.psi.impl.source.tree.LeafPsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.toTextRange
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexRedundantParInspection : TexifyRegexInspection(
    inspectionDisplayName = "Redundant use of \\par",
    inspectionId = "RedundantPar",
    errorMessage = { "Use of \\par is redundant here" },
    pattern = Pattern.compile("((\\s*\\n\\s*\\n\\s*(\\\\par))|(\\n\\s*(\\\\par)\\s*\\n)|((\\\\par)\\s*\\n\\s*\\n))"),
    replacement = { _, _ -> "" },
    replacementRange = this::parRange,
    quickFixName = { "Remove \\par" },
    highlightRange = { parRange(it).toTextRange() }
) {

    companion object {

        fun parRange(it: Matcher) = when {
            it.group(3) != null -> it.groupRange(3)
            it.group(5) != null -> it.groupRange(5)
            else -> it.groupRange(7)
        }
    }

    override fun checkContext(matcher: Matcher, element: PsiElement): Boolean {
        val elt = element.containingFile.findElementAt(matcher.end()) as? LeafPsiElement
        return super.checkContext(matcher, element) && elt?.elementType != (LatexTypes.COMMAND_TOKEN ?: true)
    }
}

package nl.hannahsten.texifyidea.inspections.latex.redundancy

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.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexRequiredParam
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.firstChildOfType
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.trimRange
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexTrimWhitespaceInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "TrimWhitespace"

    override val ignoredSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Unnecessary whitespace in section commands"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.commandsInFile()
        for (command in commands) {
            if (command.name !in CommandMagic.sectionMarkers) {
                continue
            }

            val sectionName = command.firstChildOfType(LatexRequiredParam::class)?.text?.trimRange(1, 1) ?: continue
            if (!PatternMagic.excessWhitespace.matcher(sectionName).matches()) {
                continue
            }

            val name = command.name ?: command.commandToken.text
            descriptors.add(
                manager.createProblemDescriptor(
                    command,
                    TextRange.from(name.length + 1, sectionName.length),
                    "Unnecessary whitespace",
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly,
                    TrimFix()
                )
            )
        }

        return descriptors
    }

    /**
     * @author Hannah Schellekens
     */
    private class TrimFix : LocalQuickFix {

        override fun getFamilyName() = "Trim whitespace"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val document = command.containingFile.document() ?: return
            val param = command.firstChildOfType(LatexRequiredParam::class) ?: return

            val start = param.textOffset + 1
            val end = param.endOffset() - 1

            val newString = document.getText(TextRange(start, end)).trim()
            document.replaceString(start, end, newString)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexCdotInspection : TexifyRegexInspection(
    inspectionDisplayName = "Use of . instead of \\cdot",
    inspectionId = "Cdot",
    errorMessage = { "\\cdot expected" },
    pattern = Pattern.compile("\\s+(\\.)\\s+"),
    mathMode = true,
    replacement = { _, _ -> "\\cdot" },
    replacementRange = { it.groupRange(1) },
    quickFixName = { "Change to \\cdot" }
)

package nl.hannahsten.texifyidea.inspections.latex.typesetting

import com.intellij.codeInspection.ProblemDescriptor
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.parser.parentOfType
import java.util.regex.Pattern
import kotlin.math.max

/**
 * @author Hannah Schellekens
 */
open class LatexCiteBeforePeriodInspection : TexifyRegexInspection(
    inspectionDisplayName = "Citations must be placed before interpunction",
    inspectionId = "CiteBeforePeriod",
    errorMessage = { "\\cite is placed after interpunction" },
    pattern = Pattern.compile("([.,?!;:])~(\\\\cite)"),
    mathMode = false,
    replacement = { _, _ -> "" },
    replacementRange = { it.groupRange(1) },
    highlightRange = { it.groupRange(2).toTextRange() },
    quickFixName = { "Move interpunction to the back of \\cite" },
    groupFetcher = { listOf(it.group(1)) },
    cancelIf = { matcher, psiFile ->
        // Let's assume that an abbreviation before a cite which is not directly before a cite does not appear within n characters before the cite
        val range = matcher.groupRange(0)
        val subString = psiFile.text.substring(max(range.first - 6, 0), range.last)
        PatternMagic.abbreviation.toRegex().find(subString)?.groups?.isNotEmpty() == true || PatternMagic.unRegexableAbbreviations.any { subString.contains(it) }
    }
) {

    override fun applyFix(descriptor: ProblemDescriptor, replacementRange: IntRange, replacement: String, groups: List<String>): Int {
        val file = descriptor.psiElement.containingFile
        val document = file.document() ?: return 0
        val cite = file.findElementAt(replacementRange.last + 3)?.parentOfType(LatexCommands::class) ?: return 0

        // Find the interpunction character (the first regex group) in order to move it after the cite
        val char = groups[0]

        if (cite.endOffset() >= document.textLength || document[cite.endOffset()] != char) {
            document.insertString(cite.endOffset(), char)
        }

        super.applyFix(descriptor, replacementRange, replacement, groups)

        // The document length did not change, so the increase is 0
        return 0
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.endOffset
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexCollapseCiteInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "CollapseCite"

    override val ignoredSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.COMMAND)!!

    override fun getDisplayName() = "Collapse cite commands"

    override fun inspectFile(
        file: PsiFile,
        manager: InspectionManager,
        isOntheFly: Boolean
    ): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.commandsInFile()
            .filter { it.name in CommandMagic.bibliographyReference }

        for (cmd in commands) {
            val bundle = cmd.findCiteBundle().filter { it.getOptionalParameterMap().isEmpty() }
            if (bundle.size < 2 || !bundle.contains(cmd)) {
                continue
            }

            descriptors.add(
                manager.createProblemDescriptor(
                    cmd,
                    "Citations can be collapsed",
                    InspectionFix(bundle.map { SmartPointerManager.createPointer(it) }),
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )
        }

        return descriptors
    }

    private fun LatexCommands.findCiteBundle(): List<LatexCommands> {
        val bundle: MutableList<LatexCommands> = ArrayList()

        // Lookbefore
        var foundBefore: LatexCommands? = previousCite()
        while (foundBefore != null) {
            bundle.add(foundBefore)
            foundBefore = foundBefore.previousCite()
        }

        // Current
        bundle.add(this)

        // Lookahead
        var foundAfter: LatexCommands? = nextCite()
        while (foundAfter != null) {
            bundle.add(foundAfter)
            foundAfter = foundAfter.nextCite()
        }

        return bundle
    }

    private fun LatexCommands.nextCite() = searchCite { it.nextSiblingIgnoreWhitespace() as? LatexNoMathContent }

    private fun LatexCommands.previousCite() = searchCite { it.previousSiblingIgnoreWhitespace() as? LatexNoMathContent }

    /**
     * Search for a cite command in the "direction" of [nextThing]. This cite command should be similar to this with
     * respect to the following:
     *  - Same cite command (e.g., both `\cite` or both `\footcite`).
     *  - Both are or both are not the starred version.
     *
     * @param nextThing function to get the next psi element that is searched for a cite command.
     *
     * @return null if no suitable cite command is found.
     */
    private inline fun LatexCommands.searchCite(nextThing: (LatexNoMathContent) -> LatexNoMathContent?): LatexCommands? {
        // The cite commands are not direct siblings, but their grandparents are.
        val content = firstParentOfType(LatexNoMathContent::class) ?: return null
        val nextContent = nextThing(content) ?: return null

        var cite = nextContent.firstChild
        // If there is no command inside the sibling grandparent, it can still be a non-breaking space.
        if (cite !is LatexCommands) {
            // If it is a non-breaking space, we want to check if the next grandparent sibling contains a cite.
            if (nextContent.isNonBreakingSpace()) {
                val secondNextContent = nextThing(nextContent) ?: return null
                cite = secondNextContent.firstChild ?: return null
            }
            // If it is not a non-breaking space it is some other text and we won't find another cite in this direction.
            else return null
        }

        // Check if the found command is a similar cite command as the one we started at.
        if (cite !is LatexCommands) return null
        val name = cite.name ?: return null
        val nextCommandIsACitation = name in CommandMagic.bibliographyReference
        val previousCommandIsOfTheSameType = this.name == name
        val equalStars = hasStar() == cite.hasStar()
        return if (nextCommandIsACitation && previousCommandIsOfTheSameType && equalStars) cite else null
    }

    /**
     * Check if [LatexContent] is a non breaking space.
     */
    private fun LatexNoMathContent.isNonBreakingSpace(): Boolean {
        val normalText = firstChildOfType(LatexNormalText::class) ?: return false
        return normalText.text == "~"
    }

    /**
     * @property citeBundle a bundle of cite commands that have to be merged into one command. All commands
     * in this bundle have only required parameters, as cites with optional parameters should not
     * be collapsed.
     */
    private inner class InspectionFix(val citeBundle: List<SmartPsiElementPointer<LatexCommands>>) : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Collapse citations"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val sortedBundle = citeBundle.mapNotNull { it.element }.sortedBy { it.textOffset }
            // The bundle can contain a gap when the cite commands in it surround a cite command
            // that is not in the bundle, e.g., a cite command that has an optional parameter.
            val bundleContainsGap = sortedBundle
                .zipWithNext { a, b -> a.nextSibling != b }
                .any()

            // Create the content of the required parameter of the new cite command.
            val bundle = sortedBundle
                .flatMap { it.getRequiredParameters() }
                .joinToString(",")

            // Find the cite command that has to be replaced. When the bundle contains a gap, this is the command
            // underneath the caret.
            val targetCite = if (bundleContainsGap) {
                val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return
                sortedBundle.firstOrNull { it.endOffset >= editor.caretOffset() }
                    // When something went wrong with finding a cite at the caret we target the last of the cites
                    // based on the assumption that cites that have an optional argument are more specific and/or
                    // important cites and "should" come first.
                    ?: sortedBundle.lastOrNull() ?: return
            }
            // When the bundle does not contain a gap this is the first command, as it doesn't matter
            // whichever one we pick.
            else sortedBundle.firstOrNull() ?: return

            // Construct the entire text of the new cite command.
            val star = if (targetCite.hasStar()) "*" else ""
            val replacement = "${targetCite.name}$star{$bundle}"

            val psiHelper = LatexPsiHelper(project)
            for (cite in sortedBundle) {
                // Replace the target cite with the new cite command, using the psi tree.
                if (cite == targetCite) {
                    cite.replace(psiHelper.createFromText(replacement).firstChild)
                }
                // Remove any other cite from the psi tree.
                else cite.parent.node.removeChild(cite.node)
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.source.tree.LeafPsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.lang.Diacritic
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexMathContent
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.parser.hasParent
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.parser.isComment
import java.util.*
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexDiacriticIJInspection : TexifyRegexInspection(
    inspectionDisplayName = "Dotless versions of i and j should be used with diacritics",
    inspectionId = "DiacriticIJ",
    highlight = ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
    errorMessage = { "Diacritic must be placed upon a dotless ${letter(it)}" },
    pattern = Pattern.compile(
        "(${Diacritic.allValues().joinToString("|") {
            it.command.replace("\\", "\\\\")
                .replace("^", "\\^")
                .replace(".", "\\.")
        }})\\{?([ij])}?"
    ),
    replacement = this::replacement,
    replacementRange = this::replaceRange,
    quickFixName = { "Change to dotless ${letter(it)}" }
) {

    companion object {

        fun replacement(it: Matcher, file: PsiFile): String {
            val group = it.group(2)
            val element = file.findElementAt(it.start())

            // Math mode.
            if (element != null && element.inMathContext()) {
                return when (group) {
                    "i" -> "\\imath"
                    else -> "\\jmath"
                }
            }

            // Regular text.
            return when (it.group(2)) {
                "i" -> "{\\i}"
                else -> "{\\j}"
            }
        }

        fun replaceRange(it: Matcher) = it.groupRange(2)

        fun letter(it: Matcher) = it.group(2)!!
    }

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.COMMAND)!!

    override fun checkContext(matcher: Matcher, element: PsiElement): Boolean {
        if (element.isComment()) return false

        val file = element.containingFile
        val offset = matcher.end()

        val foundAhead = file.findElementAt(offset)
        if (foundAhead is LeafPsiElement && foundAhead.elementType == LatexTypes.COMMAND_TOKEN) {
            return false
        }

        val found = file.findElementAt(offset - 1) ?: return false
        return found.hasParent(LatexNormalText::class) || found.hasParent(LatexMathContent::class)
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.length
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.toTextRange
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexEnDashInspection : TexifyRegexInspection(
    inspectionDisplayName = "En dash in number ranges",
    inspectionId = "EnDash",
    errorMessage = { "En dash expected" },
    pattern = Pattern.compile("(?<![0-9\\-])\\s+(([0-9]+)\\s*[\\- ]+\\s*([0-9]+))(\\s+|.)(?=[^0-9\\-])")!!,
    quickFixName = { "Convert to en dash" },
    cancelIf = { matcher, _ -> PatternMagic.correctEnDash.matcher(matcher.group(1)).matches() },
    replacementRange = { it.groupRange(1) },
    highlightRange = { it.groupRange(1).toTextRange() },
    groupFetcher = { listOf(it.group(2), it.group(3)) }
) {

    override fun applyFix(descriptor: ProblemDescriptor, replacementRange: IntRange, replacement: String, groups: List<String>): Int {
        val file = descriptor.psiElement as PsiFile
        val document = file.document() ?: return 0
        val start = groups[0]
        val end = groups[1]

        val dashReplacement = "$start--$end"
        document.replaceString(replacementRange.first, replacementRange.last, dashReplacement)

        return dashReplacement.length - replacementRange.length
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.*
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyLineOptionsInspection
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexDisplayMath
import nl.hannahsten.texifyidea.psi.LatexInlineMath
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.endOffset
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.get
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import java.util.*

/**
 * @author Hannah Schellekens
 */
open class LatexEncloseWithLeftRightInspection : TexifyLineOptionsInspection("Custom commands") {

    companion object {

        private val brackets = mapOf(
            "(" to ")",
            "[" to "]"
        )
    }

    override val inspectionGroup: InsightGroup
        get() = InsightGroup.LATEX

    override val inspectionId = "EncloseWithLeftRight"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.COMMAND)!!

    override fun getDisplayName() = "Enclose high lines with \\leftX..\\rightX"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()
        val document = file.document() ?: return descriptors

        val mathModes = file.childrenOfType(LatexInlineMath::class) + file.childrenOfType(LatexDisplayMath::class)
        for (mathMode in mathModes) {
            // Scan all characters in math mode. (+2/-2 to ignore \[ and \]).
            for (openOffset in mathMode.textOffset + 2 until mathMode.endOffset() - 2) {
                val char = document[openOffset]
                if (!brackets.containsKey(char)) {
                    continue
                }
                if (ignore(document, openOffset)) {
                    continue
                }

                val closeOffset = seek(document, openOffset, file) ?: continue
                val openElement = file.findElementAt(openOffset) ?: continue
                val closeElement = file.findElementAt(closeOffset) ?: continue
                // Create one fix that is passed to both the descriptors because this fix shares some state: it needs to
                // know whether it has been applied or not.
                val fix = InsertLeftRightFix(SmartPointerManager.createPointer(openElement), SmartPointerManager.createPointer(closeElement), document[openOffset])

                descriptors.add(
                    manager.createProblemDescriptor(
                        openElement,
                        TextRange.from(0, 1),
                        "Parentheses pair could be replaced by \\left(..\\right)",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        fix
                    )
                )

                descriptors.add(
                    manager.createProblemDescriptor(
                        closeElement,
                        TextRange.from(0, 1),
                        "Parentheses pair could be replaced by \\left(..\\right)",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        fix
                    )
                )
            }
        }

        return descriptors
    }

    private fun ignore(document: Document, offset: Int): Boolean {
        if (document.textLength < 6 || offset + 6 >= document.textLength) {
            return false
        }

        // Lookbehind for \left or \right.
        if (offset - 6 >= 0) {
            if (document[offset - 5 until offset] == "\\left" || document[offset - 6 until offset] == "\\right") {
                return true
            }
        }

        return false
    }

    /**
     * Check if the command that starts at 'offset' is a command which takes up vertical space, such that it should be enclosed with left./right.
     */
    private fun hasAffectedCommand(document: Document, offset: Int): Boolean {
        for (cmd in affectedCommands()) {
            if (offset + cmd.length <= document.textLength) {
                val found = document[offset until offset + cmd.length]
                if (found == cmd) {
                    return true
                }
            }
        }

        return false
    }

    private fun seek(document: Document, offset: Int, file: PsiFile): Int? {
        // Scan document.
        val open = document[offset]
        val close = brackets[open]

        var current = offset
        var nested = 0
        var closeOffset: Int? = null
        var foundAffectedCommand = false

        while (++current < file.textLength) {
            val char = document[current]

            // Ignore comments.
            val element = file.findElementAt(current)
            if (element is PsiComment) {
                continue
            }

            if (!element!!.inMathContext() && element !is PsiWhiteSpace) {
                break
            }

            // Check if it is part of a \left or \right.
            if (ignore(document, current)) {
                continue
            }

            // Check if there is an affected command.
            if (char == "\\" && hasAffectedCommand(document, current)) {
                foundAffectedCommand = true
            }

            // Open nesting
            if (char == open) {
                nested++
                continue
            }

            // Close nesting
            if (char == close && nested > 0) {
                nested--
                continue
            }

            // Whenever met at correct closure
            if (char == close) {
                closeOffset = current
                break
            }
        }

        if (!foundAffectedCommand || closeOffset == null) {
            return null
        }

        return closeOffset
    }

    private fun affectedCommands() = CommandMagic.high + lines

    /**
     * Use references to the [PsiElement]s of the open and close brackets because text offsets are not reliable when
     * applying all fixes.
     *
     * @author Hannah Schellekens
     */
    private open class InsertLeftRightFix(val openElement: SmartPsiElementPointer<PsiElement>, val closeElement: SmartPsiElementPointer<PsiElement>, val open: String) : LocalQuickFix {

        /**
         * Keep track of whether this fix has been applied.
         *
         * There are two descriptors per fix (one for the open bracket and one for the close bracket). Whenever the user
         * applies one of those fixes by hand, all is good. But when the user uses the "fix all" action, both fixes will
         * be applied. Which, when not keeping track of a fix being applied or not, means that the fix will be applied twice.
         */
        private var applied = false

        override fun getFamilyName() = "Convert (..) to \\left(..\\right)"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            if (!applied) {
                val psiHelper = LatexPsiHelper(project)
                val openReplacement = psiHelper.createFromText("\\left$open").firstChild
                val closeReplacement = psiHelper.createFromText("\\right${brackets[open]}").firstChild

                // Get the elements from the psi pointers before replacing elements, because the pointers are not accurate
                // anymore after changing the psi structure.
                val openElement = openElement.element ?: return
                val closeElement = closeElement.element ?: return

                openElement.replace(openReplacement)
                closeElement.replace(closeReplacement)

                applied = true
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

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.inspections.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.openedTextEditor
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<ProblemDescriptor> {
        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.GENERIC_ERROR_OR_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.openedTextEditor()?.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""")
        )
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

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.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexDisplayMath
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.name

/**
 * @author Sten Wessel
 */
open class LatexQedHereInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override fun getDisplayName() = "Insert \\qedhere in trailing displaymath environment"

    override val inspectionId = "QedHere"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        // Only proof environments
        val displayMaths = file.childrenOfType(LatexEnvironment::class).asSequence()
            .filter { it.name()?.text == "proof" }
            // With no \qedhere command already present
            .filterNot { it.childrenOfType(LatexCommands::class).any { cmd -> cmd.name == "\\qedhere" } }
            // Ending in a displaymath environment
            .mapNotNull { it.environmentContent?.lastChild?.firstChild?.firstChild as? LatexDisplayMath }

        for (displayMath in displayMaths) {
            val mathContent = displayMath.mathContent ?: continue
            val offset = mathContent.startOffsetInParent + mathContent.textLength - 1

            descriptors.add(
                manager.createProblemDescriptor(
                    displayMath,
                    TextRange(offset, offset + 1),
                    "Missing \\qedhere in trailing displaymath environment",
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly,
                    InsertQedHereFix()
                )
            )
        }

        return descriptors
    }

    /**
     * @author Sten Wessel
     */
    private class InsertQedHereFix : LocalQuickFix {

        override fun getFamilyName() = "Insert \\qedhere"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val element = (descriptor.psiElement as? LatexDisplayMath)?.mathContent ?: return
            val file = element.containingFile
            val document = PsiDocumentManager.getInstance(project).getDocument(file) ?: return

            document.insertString(element.textOffset + element.textLength, " \\qedhere")
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

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 com.intellij.psi.util.PsiTreeUtil.findChildrenOfType
import com.intellij.refactoring.suggested.extend
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.magic.PatternMagic.quotePattern
import nl.hannahsten.texifyidea.util.replaceString
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import nl.hannahsten.texifyidea.util.toTextRange

/**
 * @author Michael Milton
 */
class LatexQuoteInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX
    override val inspectionId = "Quote"

    /**
     * Defines how to extract a quote pair from a command.
     * @param command The LaTeX command to extract a new quote pair from
     * @param openParam The index of the argument to that command where the opening quote is defined
     * @param closeParam The index of the argument to that command where the closing quote is defined
     */
    data class QuoteMaker(val command: String, val openParam: Int, val closeParam: Int)

    /**
     * All the ways that csquotes defines a new quotation mark
     */
    private val quoteMakers = arrayOf(
        QuoteMaker("\\MakeOuterQuote", 0, 0),
        QuoteMaker("\\MakeInnerQuote", 0, 0),
        QuoteMaker("\\MakeAutoQuote", 0, 1),
        QuoteMaker("\\MakeForeignQuote", 1, 2),
        QuoteMaker("\\MakeBlockQuote", 0, 2),
        QuoteMaker("\\MakeForeignBlockQuote", 1, 3),
        QuoteMaker("\\MakeHyphenBlockQuote", 1, 3),
        QuoteMaker("\\MakeHybridBlockQuote", 1, 3),
    )

    private val fixers = arrayOf(
        MathFix(),
        LatexQuoteFix("opening single quote", "`"),
        LatexQuoteFix("opening double quote", "``"),
        LatexQuoteFix("closing single quote", "'"),
        LatexQuoteFix("closing double quote", "''"),
    )

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        // Build up a list of acceptable quote pairs. The ith index in both arrays must contain a matching quote
        // pair. Additional quote pairs are extracted from the csquotes quote making commands
        val validQuotes = arrayListOf(
            "`" to "'",
            "``" to "''",
        )
        val commands = file.commandsInFileSet()
        for (quoteMaker in quoteMakers) {
            for (command in commands) {
                val opener = command.requiredParameter(quoteMaker.openParam)
                val closer = command.requiredParameter(quoteMaker.closeParam)

                if (!opener.isNullOrEmpty() && !closer.isNullOrEmpty()) {
                    validQuotes.add(opener to closer)
                }
            }
        }
        val openers = LinkedHashSet(validQuotes.map { it.first })
        val closers = LinkedHashSet(validQuotes.map { it.second })

        // Process each of the quote tokens individually

        // Each index corresponds to a quote pair in validQuotes
        val unclosedQuoteIndexStack = ArrayDeque<Int>()
        val issues = descriptorList()
        for (text in findChildrenOfType(file, LatexNormalText::class.java)) {
            if (text.inMathContext()) {
                continue
            }
            for (match in quotePattern.findAll(text.text)) {
                val openQuoteIndex = openers.indexOf(match.value)
                if (openQuoteIndex > -1) {
                    // First case: we find a valid opener
                    // In that case, push the ID of that opener onto the stack
                    unclosedQuoteIndexStack.add(openQuoteIndex)
                }
                else {
                    val closeQuoteIndex = closers.indexOf(match.value)
                    if (closeQuoteIndex == -1) {
                        // Second case: we find a token that's neither a valid opener nor closer
                        issues.add(
                            manager.createProblemDescriptor(
                                text,
                                match.range.toTextRange(),
                                "${match.value} is not a valid set of LaTeX quotes",
                                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                                isOntheFly,
                                *fixers
                            )
                        )
                    }
                    else if (!unclosedQuoteIndexStack.isEmpty() && closeQuoteIndex == unclosedQuoteIndexStack.last()) {
                        // Third case: we find a closer that's correctly paired.
                        // In this case, we can pop the stack.
                        unclosedQuoteIndexStack.removeLast()
                    }
                    else if (match.value == "'" && match.range.first > 0 && !text.text[match.range.first - 1].isWhitespace()) {
                        // Fourth case: it looks like an unpaired quote, but it's impossible to tell because it would
                        // also be a valid apostrophe (e.g. it's preceded by non-whitespace), so we ignore it.
                        continue
                    }
                    else {
                        // Fifth case: we find a closer that's incorrectly paired
                        // We never raise this for single ' characters because these are valid apostrophes
                        issues.add(
                            manager.createProblemDescriptor(
                                text,
                                match.range.toTextRange(),
                                "Closing quote without opening quote",
                                ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                                isOntheFly,
                                *fixers
                            )
                        )
                    }
                }
            }
        }
        return issues
    }

    /**
     * Fixes an invalid quote by putting it into an inline math environment
     */
    private class MathFix : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Convert to inline maths environment, for typesetting feet, inches or other mathematical punctuation."
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val document = descriptor.psiElement.containingFile.document() ?: return
            val expandedRange = descriptor.textRangeInElement.extend(descriptor.psiElement.text) {
                it.isDigit() || it in """`"'"""
            }
            val originalText = expandedRange.substring(descriptor.psiElement.text)
            document.replaceString(expandedRange.shiftRight(descriptor.psiElement.textOffset), "\\(${originalText}\\)")
        }
    }

    private class LatexQuoteFix(val description: String, val replacement: String) : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Replace with a LaTeX $description"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val document = descriptor.psiElement.containingFile.document() ?: return
            document.replaceString(
                // The text range in the document is the text range in the containing element, shifted forward by
                // the offset of the containing element in the document
                descriptor.textRangeInElement.shiftRight(descriptor.psiElement.textOffset),
                replacement
            )
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.get
import nl.hannahsten.texifyidea.util.insertUsepackage
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import java.util.regex.Matcher

/**
 * Highlights uses of mathematical symbols composed of a colon with a equality-like relational symbol, where the colon
 * will appear vertically misaligned. The `mathtools` package provides a solution with substituting commands.
 *
 * See also Section 3.7.2 of the `mathtools` package documentation.
 *
 * @author Sten Wessel
 */
open class LatexVerticallyCenteredColonInspection : TexifyRegexInspection(
    inspectionDisplayName = "Vertically uncentered colon",
    inspectionId = "VerticallyCenteredColon",
    errorMessage = { "Colon is vertically uncentered" },
    highlight = ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
    pattern = REGEX,
    mathMode = true,
    replacement = this::replacement,
    replacementRange = { it.groupRange(0) },
    quickFixName = { "Change to ${PATTERNS[it.group(0).replace(WHITESPACE, "")]!!.command} (mathtools)" },
    cancelIf = { _, file ->
        // Per mathtools documentation, colons are automatically centered when this option is set.
        // It is impossible to determine whether this option is actually set (think scoping, but this option can also be
        // turned of with \mathtoolsset{centercolon=false})
        // Thus, whenever someone fiddles with this, we turn off the inspection to prevent false positives.
        file.commandsInFileSet().any { it.name == "\\mathtoolsset" && it.requiredParameter(0)?.contains("centercolon") == true }
    }
) {

    private data class Pattern(val regex: String, val command: String)

    companion object {

        // Whitespace in between is matched, except for newlines (we have to draw the line somewhere...)
        private val PATTERNS = mapOf(
            ":=" to Pattern(""":[^\S\r\n]*=""", "\\coloneqq"),
            "::=" to Pattern(""":[^\S\r\n]*:[^\S\r\n]*=""", "\\Coloneqq"),
            ":-" to Pattern(""":[^\S\r\n]*-""", "\\coloneq"),
            "::-" to Pattern(""":[^\S\r\n]*:[^\S\r\n]*-""", "\\Coloneq"),
            "=:" to Pattern("""=[^\S\r\n]*:""", "\\eqqcolon"),
            "=::" to Pattern("""=[^\S\r\n]*:[^\S\r\n]*:""", "\\Eqqcolon"),
            "-:" to Pattern("""-[^\S\r\n]*:""", "\\eqcolon"),
            "-::" to Pattern("""-[^\S\r\n]*:[^\S\r\n]*:""", "\\Eqcolon"),
            ":\\approx" to Pattern(""":[^\S\r\n]*\\approx(?![a-zA-Z])""", "\\colonapprox"),
            "::\\approx" to Pattern(""":[^\S\r\n]*:[^\S\r\n]*\\approx(?![a-zA-Z])""", "\\Colonapprox"),
            ":\\sim" to Pattern(""":[^\S\r\n]*\\sim(?![a-zA-Z])""", "\\colonsim"),
            "::\\sim" to Pattern(""":[^\S\r\n]*:[^\S\r\n]*\\sim(?![a-zA-Z])""", "\\Colonsim"),
            "::" to Pattern(""":[^\S\r\n]*:""", "\\dblcolon"),
        )

        private val WHITESPACE = """[^\S\r\n]+""".toRegex()

        private val REGEX = PATTERNS.values.joinToString(prefix = "(", separator = "|", postfix = ")") { it.regex }.toPattern()

        fun replacement(matcher: Matcher, file: PsiFile): String {
            val replacement = PATTERNS[matcher.group(0).replace(WHITESPACE, "")]!!.command

            // If the next character would be a letter, it would mess up the command that is inserted: append a space as well
            if (file.document()?.get(matcher.end())?.getOrNull(0)?.isLetter() == true) {
                return "$replacement "
            }

            return replacement
        }
    }

    override fun applyFixes(
        descriptor: ProblemDescriptor,
        replacementRanges: List<IntRange>,
        replacements: List<String>,
        groups: List<List<String>>
    ) {
        super.applyFixes(descriptor, replacementRanges, replacements, groups)

        // We override applyFixes instead of applyFix because all fixes need to be applied together, and only after that we insert any required package.
        val file = descriptor.psiElement.containingFile ?: return
        file.insertUsepackage(LatexPackage.MATHTOOLS)
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting

import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexXInsteadOfTimesInspection : TexifyRegexInspection(
    inspectionDisplayName = "Use of x instead of \\times",
    inspectionId = "XInsteadOfTimes",
    errorMessage = { "\\times expected" },
    pattern = Pattern.compile("[0-9]\\s+(x)\\s+[0-9]"),
    mathMode = true,
    replacement = { _, _ -> "\\times" },
    replacementRange = { it.groupRange(1) },
    quickFixName = { "Change to \\times" }
)

package nl.hannahsten.texifyidea.run.bibtex.logtab

object BibtexLogMagicRegex {

    val bibFileOpened =
        """^Database file #(\d+): (?<file>.+)$""".toRegex()
    val biberFileOpened =
        """^INFO - Found BibTeX data source '(?<file>.+)'$""".toRegex()

    val bibtexNumberOfWarnings =
        """\(There (was 1 warning|were (?<number>\d+) warnings)\)""".toRegex()
    val bibtexNumberOfErrors =
        """\(There (was 1 error message|were (?<number>\d+) error messages)\)""".toRegex()

    val biberNumberOfWarnings =
        """INFO - WARNINGS: (?<number>\d+)""".toRegex()
    val biberNumberOfErrors =
        """INFO - ERRORS: (?<number>\d+)""".toRegex()

    /*
     * Errors
     */
    val auxErrPrint =
        """---line (?<line>\d+) of file (?<file>.+)$""".toRegex()
    val cleanUpAndLeave =
        """^Aborted at line (?<line>\d+) of file (?<file>.+)$""".toRegex()
    val auxEndErr =
        """^(?<message>I found no .+)---while reading file (?<file>.+)$""".toRegex()
    val bstExWarnPrint =
        """while executing$auxErrPrint""".toRegex() // line 6874
    val nonexistentCrossReferenceError =
        """^refers to entry .+, which doesn't exist$""".toRegex()

    /*
     * Warnings
     */
    val bibLnNumPrint =
        """--line (?<line>\d+) of file (?<file>.+)\s*$""".toRegex()
    val warning =
        """^Warning--(?<message>.+)\s*$""".toRegex()
    val noFields =
        """^Warning--(?<message>.+)$bibLnNumPrint\s*$""".toRegex()
    val nestedCrossReference =
        """^Warning--you've nested cross references--entry .+$""".toRegex()

    /*
     * Biber
     */
    val biberError =
        """^ERROR - (?<message>.+)$""".toRegex()
    val biberErrorBibtexSubsystem =
        """^ERROR - BibTeX subsystem: .+, line (?<line>\d+), (?<message>.+)$""".toRegex()
    val biberWarning =
        """^WARN - (?<message>.+)$""".toRegex()
    val biberWarningBibtexSubsystem =
        """^WARN - BibTeX subsystem: .+, line (?<line>\d+), warning: (?<message>.+)$""".toRegex()
    val biberWarningInFile =
        """^WARN - (?<message>.+) in file .+, skipping ...$""".toRegex()
}

package nl.hannahsten.texifyidea.run.bibtex.logtab

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.ui.MessageCategory
import nl.hannahsten.texifyidea.util.capitalizeFirst
import java.util.*

data class BibtexLogMessage(val message: String, val fileName: String? = null, val line: Int? = -1, val type: BibtexLogMessageType, val file: VirtualFile? = null) {

    fun toTreeViewString(): String {
        val typeString = type.toString().lowercase(Locale.getDefault()).capitalizeFirst()
        val lineString = if (line != null && line >= 0) "line ($line)" else ""
        return "$typeString:$lineString $message"
    }
}

enum class BibtexLogMessageType(val category: Int) {

    ERROR(MessageCategory.ERROR),
    WARNING(MessageCategory.WARNING);

    override fun toString(): String {
        return super.toString().lowercase(Locale.getDefault())
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab

import com.intellij.diagnostic.logging.AdditionalTabComponent
import com.intellij.execution.process.ProcessHandler
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.latex.logtab.ui.LatexCompileMessageTreeView
import java.awt.BorderLayout
import javax.swing.JComponent

/**
 * Similar to [nl.hannahsten.texifyidea.run.latex.logtab.LatexLogTabComponent].
 */
class BibtexLogTabComponent(val project: Project, val mainFile: VirtualFile?, startedProcess: ProcessHandler) : AdditionalTabComponent(BorderLayout()) {

    private val bibtexMessageList = mutableListOf<BibtexLogMessage>()
    private val treeView = LatexCompileMessageTreeView(project, mutableListOf(), bibtexMessageList)

    init {
        add(treeView, BorderLayout.CENTER)
        startedProcess.addProcessListener(BibtexOutputListener(project, mainFile, bibtexMessageList, treeView), this)
    }

    override fun getTabTitle() = "Log Messages"

    override fun dispose() {
    }

    override fun getPreferredFocusableComponent() = component

    override fun getToolbarActions(): ActionGroup? = null

    override fun getToolbarContextComponent(): JComponent? = null

    override fun getToolbarPlace(): String? = null

    override fun getSearchComponent(): JComponent? = null

    override fun isContentBuiltIn() = false
}

package nl.hannahsten.texifyidea.run.bibtex.logtab

abstract class BibtexMessageHandler {

    /**
     * Find a message from the given window, last added line is last in the list.
     *
     * @param currentFile: Currently open bib file.
     */
    abstract fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage?
}

package nl.hannahsten.texifyidea.run.bibtex.logtab

import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.errors.*
import nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.warnings.*
import nl.hannahsten.texifyidea.run.latex.logtab.ui.LatexCompileMessageTreeView
import nl.hannahsten.texifyidea.util.files.findFile
import org.apache.commons.collections.Buffer
import org.apache.commons.collections.BufferUtils
import org.apache.commons.collections.buffer.CircularFifoBuffer

/**
 *
 *
 * http://tug.org/svn/texlive/trunk/Build/source/texk/web2c/bibtex.web?view=markup
 * http://tug.ctan.org/tex-archive/biblio/bibtex/base/bibtex.web
 *
 * WEAVE: http://texdoc.net/texmf-dist/doc/generic/knuth/web/weave.pdf
 *
 * (MIT license) https://github.com/aclements/latexrun/blob/38ff6ec2815654513c91f64bdf2a5760c85da26e/latexrun#L1727
 *
 * https://github.com/lervag/vimtex/blob/master/autoload/vimtex/qf/bibtex.vim
 */
class BibtexOutputListener(
    val project: Project,
    val mainFile: VirtualFile?,
    private val messageList: MutableList<BibtexLogMessage>,
    val treeView: LatexCompileMessageTreeView
) : ProcessListener {

    // Assume the window size is large enough to hold any message at once
    var window: Buffer = BufferUtils.synchronizedBuffer(CircularFifoBuffer(5))

    /** Currently open bib file. */
    var currentFile: String = ""

    override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
        processNewText(event.text)
    }

    fun processNewText(newText: String) {
        window.add(newText)

        updateCurrentFile(newText)

        val windowList: List<String> = window.mapNotNull { it as? String }
        val logMessage = extractMessage(windowList) ?: return

        if (!messageList.contains(logMessage)) {
            val file = mainFile?.parent?.findFile(logMessage.fileName ?: mainFile.name)
            val messageWithFile = BibtexLogMessage(logMessage.message, logMessage.fileName, logMessage.line, logMessage.type, file)
            messageList.add(messageWithFile)
            addBibMessageToTree(messageWithFile)
        }
    }

    fun addBibMessageToTree(logMessage: BibtexLogMessage) {
        treeView.applyFilters(logMessage)
    }

    /**
     * Let all message handlers try to find a message based on the window.
     */
    private fun extractMessage(windowList: List<String>): BibtexLogMessage? {
        val bibtexErrorHandlers = listOf(
            BstExWarnPrintBibtexMessageHandler, // Should be before AuxErrPrintBibtexMessageHandler
            AuxErrPrintBibtexMessageHandler,
            CleanUpAndLeaveBibtexMessageHandler,
            AuxEndErrBibtexMessageHandler,
            NonexistentCrossReferenceBibtexMessageHandler,
            // Biber
            BiberErrorBibtexSubsystemMessageHandler,
            BiberErrorMessageHandler
        )

        // Note that these bibtex handlers are triggered on the middle line of the given window (supposing it has 5 lines)
        // to be able to look back and forward.
        val bibtexWarningHandlers = listOf(
            NoFieldsBibtexMessageHandler,
            WarningBibtexMessageHandler,
            BibLnNumPrintBibtexMessageHandler,
            // Biber
            BiberWarningBibtexSubsystemMessageHandler,
            BiberWarningMessageHandler
        )

        val handlers = bibtexErrorHandlers + bibtexWarningHandlers

        handlers.forEach { handler ->
            handler.findMessage(windowList, currentFile)?.let {
                return it
            }
        }
        return null
    }

    private fun updateCurrentFile(newText: String) {
        BibtexLogMagicRegex.bibFileOpened.find(newText)?.apply {
            currentFile = groups["file"]?.value ?: ""
        }
        BibtexLogMagicRegex.biberFileOpened.find(newText)?.apply {
            currentFile = groups["file"]?.value ?: ""
        }
    }

    override fun processTerminated(event: ProcessEvent) {
        if (event.exitCode == 0) {
            treeView.setProgressText("Compilation was successful.")
        }
        else {
            treeView.setProgressText("Compilation failed.")
        }
    }

    override fun startNotified(event: ProcessEvent) {
        treeView.setProgressText("Compilation in progress...")
    }
}

package nl.hannahsten.texifyidea.run.latex.externaltool

import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.compiler.ExternalTool

/**
 * Run an external tool.
 */
class ExternalToolCommandLineState(
    environment: ExecutionEnvironment,
    private val mainFile: VirtualFile?,
    private val workingDirectory: VirtualFile?,
    private val tool: ExternalTool
) : CommandLineState(environment) {

    @Throws(ExecutionException::class)
    override fun startProcess(): ProcessHandler {
        if (mainFile == null) {
            throw ExecutionException("Main file to compile is not found or missing.")
        }

        val command = listOf(tool.executableName, mainFile.nameWithoutExtension)
        val commandLine = GeneralCommandLine(command).withWorkDirectory(workingDirectory?.path)

        val handler: ProcessHandler = KillableProcessHandler(commandLine)

        // Reports exit code to run output window when command is terminated
        ProcessTerminatedListener.attach(handler, environment.project)

        return handler
    }
}

package nl.hannahsten.texifyidea.run.latex.externaltool

import com.intellij.execution.Executor
import com.intellij.execution.configurations.*
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.compiler.ExternalTool
import org.jdom.Element
import java.util.*

/**
 * Run configuration for running an [nl.hannahsten.texifyidea.run.compiler.ExternalTool].
 */
class ExternalToolRunConfiguration(
    project: Project,
    factory: ConfigurationFactory,
    name: String
) : RunConfigurationBase<ExternalToolCommandLineState>(project, factory, name), LocatableConfiguration {

    companion object {

        private const val PARENT_ELEMENT = "texify-external"
        private const val PROGRAM = "program"
        private const val MAIN_FILE = "main-file"
        private const val WORK_DIR = "work-dir"
    }

    // The following variables are saved in the SettingsEditor
    var program = ExternalTool.PYTHONTEX
    var mainFile: VirtualFile? = null
    var workingDirectory: VirtualFile? = null

    override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState {
        return ExternalToolCommandLineState(environment, mainFile, workingDirectory, this.program)
    }

    override fun getConfigurationEditor(): SettingsEditor<out RunConfiguration> = ExternalToolSettingsEditor(project)

    override fun readExternal(element: Element) {
        super<LocatableConfiguration>.readExternal(element)

        val parent = element.getChild(PARENT_ELEMENT)

        try {
            val programText = parent.getChildText(PROGRAM)
            if (!programText.isNullOrEmpty()) {
                this.program = ExternalTool.valueOf(programText)
            }
        }
        catch (ignored: NullPointerException) {}

        val mainFilePath = try {
            parent.getChildText(MAIN_FILE)
        }
        catch (e: NullPointerException) {
            null
        }
        mainFile = if (!mainFilePath.isNullOrBlank()) {
            LocalFileSystem.getInstance().findFileByPath(mainFilePath)
        }
        else {
            null
        }

        val workDirPath = try {
            parent.getChildText(WORK_DIR)
        }
        catch (e: NullPointerException) {
            null
        }
        workingDirectory = if (!workDirPath.isNullOrBlank()) {
            LocalFileSystem.getInstance().findFileByPath(workDirPath)
        }
        else {
            null
        }
    }

    override fun writeExternal(element: Element) {
        super<LocatableConfiguration>.writeExternal(element)

        val parent = element.getChild(PARENT_ELEMENT) ?: Element(PARENT_ELEMENT).apply { element.addContent(this) }
        parent.removeContent()

        parent.addContent(Element(PROGRAM).apply { text = this@ExternalToolRunConfiguration.program.name })
        parent.addContent(Element(MAIN_FILE).apply { text = mainFile?.path ?: "" })
        parent.addContent(Element(WORK_DIR).apply { text = workingDirectory?.path ?: "" })
    }

    override fun isGeneratedName() = name == suggestedName()

    override fun suggestedName(): String {
        val main = if (mainFile != null) mainFile?.nameWithoutExtension + " " else ""
        return main + this.program.name.lowercase(Locale.getDefault())
    }

    fun setSuggestedName() {
        name = suggestedName()
    }
}

package nl.hannahsten.texifyidea.run.latex.externaltool

import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.execution.configurations.ConfigurationType
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.run.latex.LatexConfigurationFactory
import javax.swing.Icon

/**
 * Type of [ExternalToolRunConfiguration].
 */
class ExternalToolRunConfigurationType : ConfigurationType {

    override fun getIcon(): Icon = TexifyIcons.BUILD

    override fun getConfigurationTypeDescription() = "External LaTeX tool run configuration"

    override fun getId() = "EXTERNAL_TOOL_RUN_CONFIGURATION"

    override fun getDisplayName() = "External LaTeX Tool"

    override fun getConfigurationFactories(): Array<ConfigurationFactory> = arrayOf(LatexConfigurationFactory(this))
}

package nl.hannahsten.texifyidea.run.latex.externaltool

import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileTypeDescriptor
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.*
import com.intellij.openapi.vfs.LocalFileSystem
import nl.hannahsten.texifyidea.run.compiler.ExternalTool
import javax.swing.JComponent
import javax.swing.JPanel

/**
 * @author Thomas
 */
class ExternalToolSettingsEditor(private val project: Project) : SettingsEditor<ExternalToolRunConfiguration>() {

    private lateinit var panel: JPanel
    private lateinit var program: LabeledComponent<ComboBox<ExternalTool>>
    private lateinit var mainFile: LabeledComponent<TextFieldWithBrowseButton>
    private lateinit var workingDirectory: LabeledComponent<TextFieldWithBrowseButton>

    override fun createEditor(): JComponent {
        createUIComponents()
        return panel
    }

    // Initialize editor from given run config
    override fun resetEditorFrom(runConfig: ExternalToolRunConfiguration) {
        program.component.selectedItem = runConfig.program
        mainFile.component.text = runConfig.mainFile?.path ?: ""
        workingDirectory.component.text = runConfig.workingDirectory?.path ?: ""
    }

    // Save user selected settings to the given run config
    override fun applyEditorTo(runConfig: ExternalToolRunConfiguration) {
        runConfig.program = program.component.selectedItem as ExternalTool
        runConfig.mainFile = LocalFileSystem.getInstance().findFileByPath(mainFile.component.text)
        runConfig.workingDirectory = LocalFileSystem.getInstance().findFileByPath(workingDirectory.component.text)
    }

    private fun createUIComponents() {
        panel = JPanel().apply {
            layout = VerticalFlowLayout(VerticalFlowLayout.TOP)

            // Program
            val programField = ComboBox(ExternalTool.values())
            program = LabeledComponent.create(programField, "External tool")
            add(program)

            // Main file
            val mainFileField = TextFieldWithBrowseButton().apply {
                addBrowseFolderListener(
                    TextBrowseFolderListener(
                        FileTypeDescriptor("Choose Main LaTeX File", ".tex")
                            .withRoots(*ProjectRootManager.getInstance(project).contentRootsFromAllModules)
                    )
                )
            }
            mainFile = LabeledComponent.create(mainFileField, "Main file which requires the external tool")
            add(mainFile)

            // Working directory
            val workDirField = TextFieldWithBrowseButton().apply {
                addBrowseFolderListener(
                    TextBrowseFolderListener(
                        FileChooserDescriptor(false, true, false, false, false, false)
                            .withTitle("Choose Output Directory of Main LaTeX File")
                    )
                )
            }
            workingDirectory = LabeledComponent.create(workDirField, "Working directory for the external tool")
            add(workingDirectory)
        }
    }
}

package nl.hannahsten.texifyidea.run.latex.externaltool

import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.impl.RunConfigurationBeforeRunProvider
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.run.compiler.ExternalTool
import nl.hannahsten.texifyidea.run.latex.LatexConfigurationFactory
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.includedPackages

/**
 * Run the external tool.
 */
class RunExternalToolListener(
    private val latexRunConfig: LatexRunConfiguration,
    private val environment: ExecutionEnvironment,
) : ProcessListener {

    companion object {

        /**
         * Check the contents of the LaTeX fileset to find out if any external tools are needed.
         */
        fun getRequiredExternalTools(mainFile: VirtualFile?, project: Project): Set<ExternalTool> {
            val usedPackages = runReadAction {
                mainFile?.psiFile(project)?.includedPackages() ?: emptySet()
            }

            val externalTools = mutableSetOf<ExternalTool>()

            if (LatexPackage.PYTHONTEX in usedPackages) {
                externalTools.add(ExternalTool.PYTHONTEX)
            }

            return externalTools
        }
    }

    override fun processTerminated(event: ProcessEvent) {
        try {
            // Only create new one if there is none yet
            val runConfigSettingsList =
                latexRunConfig.externalToolRunConfigs.ifEmpty {
                    generateExternalToolConfigs()
                }

            // Run all run configurations
            for (runConfigSettings in runConfigSettingsList) {
                RunConfigurationBeforeRunProvider.doExecuteTask(environment, runConfigSettings, null)
            }

            scheduleLatexRuns()
        }
        finally {
            latexRunConfig.isLastRunConfig = false
            latexRunConfig.isFirstRunConfig = true
        }
    }

    private fun scheduleLatexRuns() {
        // Don't schedule more latex runs if bibtex is used, because that will already schedule the extra runs
        if (latexRunConfig.bibRunConfigs.isEmpty() && latexRunConfig.makeindexRunConfigs.isEmpty()) {
            // LaTeX twice
            latexRunConfig.isFirstRunConfig = false
            val latexSettings = RunManagerImpl.getInstanceImpl(environment.project).getSettings(latexRunConfig)
                ?: return

            // Terrible hack to avoid running the before run tasks repeatedly
            // At least I think this is the intended behaviour, but not sure so first testing it here
            val beforeRunTasks = latexSettings.configuration.beforeRunTasks
            latexSettings.configuration.beforeRunTasks = mutableListOf()

            latexRunConfig.isLastRunConfig = false
            RunConfigurationBeforeRunProvider.doExecuteTask(environment, latexSettings, null)
            latexRunConfig.isLastRunConfig = true
            RunConfigurationBeforeRunProvider.doExecuteTask(environment, latexSettings, null)

            latexSettings.configuration.beforeRunTasks = beforeRunTasks
        }
    }

    /**
     * Generate the extra run configs if needed.
     */
    private fun generateExternalToolConfigs(): Set<RunnerAndConfigurationSettings> {
        val runManager = RunManagerImpl.getInstanceImpl(environment.project)
        val tools = getRequiredExternalTools(latexRunConfig.mainFile, environment.project)

        val runConfigs = mutableSetOf<RunnerAndConfigurationSettings>()

        for (tool in tools) {
            val runConfigSettings = runManager.createConfiguration(
                "",
                LatexConfigurationFactory(ExternalToolRunConfigurationType())
            )

            val runConfig = runConfigSettings.configuration as ExternalToolRunConfiguration

            runConfig.mainFile = latexRunConfig.mainFile
            runConfig.program = tool
            runConfig.setSuggestedName()
            runConfig.workingDirectory = latexRunConfig.getAuxilDirectory()

            runManager.addConfiguration(runConfigSettings)
            runConfigs.add(runConfigSettings)
        }

        latexRunConfig.externalToolRunConfigs = runConfigs
        return runConfigs
    }

    override fun onTextAvailable(p0: ProcessEvent, p1: Key<*>) {}

    override fun processWillTerminate(p0: ProcessEvent, p1: Boolean) {}

    override fun startNotified(p0: ProcessEvent) {}
}

package nl.hannahsten.texifyidea.run.latex.logtab

import com.intellij.openapi.diagnostic.Logger
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LINE_WIDTH
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.lineNumber
import nl.hannahsten.texifyidea.util.containsAny
import nl.hannahsten.texifyidea.util.firstIndexOfAny
import nl.hannahsten.texifyidea.util.remove
import java.util.*

class LatexFileStack(
    vararg val file: String,
    /** Number of open parentheses that do not represent file openings and have not yet been closed. */
    var notClosedNonFileOpenParentheses: Int = 0
) : ArrayDeque<String>() {

    private var shouldSkipNextLine = false

    // Usage: set to true to output all file openings/closings to idea.log
    // Then open the LaTeX log and the relevant part of idea.log in IntelliJ, and start from the idea.log parenthesis which
    // was closed incorrectly, use the LaTeX log and brace matching to find which file should have been closed, then check where it was actually closed, etc.
    private val debug = false
    private val logger = Logger.getInstance("LatexFileStack")

    init {
        addAll(file)
    }

    /** Collect file here if it spans multiple lines. */
    private var currentCollectingFile = ""

    override fun peek(): String? = if (isEmpty()) null else super.peek()

    private fun pushFile(file: String, line: String) {
        push(file)
        if (debug) logger.info("$line ---- Opening $file")
        if (file.containsAny(setOf("(", ")"))) throw TeXception("$file is probably not a valid file")
    }

    /**
     * AFTER all messages have been filtered, look for file openings and closings.
     *
     * Assume that all parenthesis come in pairs. Will probably fail the (few)
     * cases where they don't...
     * (It works for rubber: https://github.com/tsgates/die/blob/master/bin/parse-latex-log.py)
     */
    fun update(line: String): LatexFileStack {
        // Lines starting with a line number seem to contain user content, as well as the next line (which could be empty
        // as well, but in that case it isn't interesting either)
        if (lineNumber.containsMatchIn(line)) {
            shouldSkipNextLine = true
            return this
        }

        if (shouldSkipNextLine) {
            shouldSkipNextLine = false
            return this
        }

        if (currentCollectingFile.isNotEmpty()) {
            // Files may end halfway the line, or right at the end of a line
            // Assume there are no spaces in the path (how can we know where the file ends?)
            val endIndex = if (setOf(" ", ")", "(").any { it in line }) line.firstIndexOfAny(' ', ')', '(') else line.length
            currentCollectingFile += line.substring(0, endIndex).remove("\n")
            // Check if this was the last part of the file
            if (line.substring(0, endIndex).length < LINE_WIDTH) {
                // Assume that paths can be quoted, but there are no " in folder/file names
                pushFile(currentCollectingFile.trim('"'), line)
                currentCollectingFile = ""
            }
        }

        // Matches an open par with a filename, or a closing par
        // If the filepath probably continues on the next line, don't try to match the extension
        // (could be improved by actually checking the length of the 'file' group instead of line length)
        // Otherwise, match the file extension to avoid matching just text in parentheses
        val fileRegex = if (line.length >= LINE_WIDTH - 1) {
            Regex("""\((?<file>"?\.*(([/\\])*[\w-. :])+"?)|\)""")
        }
        else {
            Regex("""\((?<file>"?\.*(([/\\])*[\w-. :])+\.(\w{2,10})"?)|\)""")
        }

        var result = fileRegex.find(line)
        var linePart = line

        while (result != null) {
            // If the regex matches an open par (with filename), register file
            if (linePart[result.range.first] == '(') {
                // Count all open pars that are before the found opening par.
                if (linePart.indexOfFirst { it == '(' } in 0..result.range.first) {
                    notClosedNonFileOpenParentheses += linePart.substring(0, result.range.first).count { it == '(' }
                }
                val file = result.groups["file"]?.value?.trim() ?: break

                // Check if file spans multiple lines
                // +1 because the starting ( is not in the group
                // -1 because the newline is not here
                // Files could span exactly the full line width, but not continue on the next line
                if (!file.endsWith(".tex") && (file.length + 1 >= LINE_WIDTH - 1 || (line.length >= LINE_WIDTH && line.trim().endsWith(file)))) {
                    currentCollectingFile += file
                }
                else {
                    pushFile(file, line)
                }
            }

            // Regex has matched a closing par
            else {
                // Count all open pars that are before the found closing par.
                if (linePart.indexOfFirst { it == '(' } in 0..result.range.first) {
                    notClosedNonFileOpenParentheses += linePart.substring(0, result.range.first).count { it == '(' }
                }
                if (notClosedNonFileOpenParentheses > 0) notClosedNonFileOpenParentheses--
                else {
                    if (debug) logger.info("$line ---- Closing ${peek()}")
                    if (!isEmpty()) {
                        pop()
                    }
                }
            }

            linePart = linePart.substring(result.range.last + 1)
            result = fileRegex.find(linePart)
        }

        // When we find a closing par or no match, there can still be an open
        // parenthesis somewhere on the current line (before the closing par).
        // We want to detect this par, so we know that the next closing par does
        // not close a file.
        // This has to happen after the above while loop, to still catch leftover open brackets at the end of a line
        if (result == null) {
            notClosedNonFileOpenParentheses += linePart.count { it == '(' }
        }

        return this
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab

/**
 * Regex (text) every error handler and message processor can use.
 */
object LatexLogMagicRegex {

    // Line length (including newline at the end)
    const val LINE_WIDTH = 80

    // Match filename:linenumber: as this probably denotes an error, but not if it appears in a stacktrace
    // and starts with ... and also not if this starts with a ( as then we assume the ( is not part of the file
    const val FILE_LINE_REGEX =
        """(?!\s*\.\.\.)(?<file>[^()]+\.\w+):(?<line>\d+):""" // error
    val REPORTED_ON_LINE_REGEX =
        """( Reported| Found)? on input line (?<line>\d+).""".toRegex()
    const val LINE_REGEX: String =
        """on input line (?<line>\d+).""" // meestal warning
    const val LATEX_ERROR_REGEX: String = "!" // error
    const val PDFTEX_ERROR_REGEX: String = "!pdfTeX error:"
    const val LATEX_WARNING_REGEX: String = "LaTeX( Font)? Warning:" // warning
    const val PACKAGE_REGEX: String =
        """(?<package>[\w-.]+)""" // package error/warning?
    const val REFERENCE_REGEX: String =
        """(?<label>([`']).+')""" // reference warning
    const val PACKAGE_WARNING_CONTINUATION = "\\(\\w+\\) {${"Package warning:".length}}"
    const val DUPLICATE_WHITESPACE =
        """\s{2,}"""

    val lineNumber =
        """^l.\d+ """.toRegex()

    /*
     * Errors
     */

    /** A variation on [FILE_LINE_REGEX] by lualatex (?) */
    val directLuaError =
        """^\((?!\s*\.\.\.)(.+\.\w+)\)(\[.+])?:(?<line>\d+): (?<message>.*)""".toRegex()
    val fixMeError =
        """FiXme (Fatal )?Error: '(?<message>.+)' on input line (?<line>\d+).""".toRegex()

    /*
     * Warnings
     */
    val fixMeWarning =
        """FiXme Warning: '(?<message>.+)' on input line (?<line>\d+).""".toRegex()
    val pdfTeXWarning =
        """pdfTeX warning(.+)?: (?<message>.+)""".toRegex()

    val TEX_MISC_WARNINGS = listOf(
        "LaTeX Warning: ",
        "LaTeX Font Warning: ",
        "AVAIL list clobbered at",
        "Citation",
        "Double-AVAIL list clobbered at",
        "Doubly free location at",
        "Bad flag at",
        "Runaway definition",
        "Runaway argument",
        "Runaway text",
        "Missing character: There is no",
        "No auxiliary output files.",
        "No file ",
        "No pages of output.",
        "Underfull \\hbox",
        "Overfull \\hbox",
        "Loose \\hbox",
        "Tight \\hbox",
        "Underfull \\vbox",
        "Overfull \\vbox",
        "Loose \\vbox",
        "Tight \\vbox",
        "(\\end occurred",
    )
}

package nl.hannahsten.texifyidea.run.latex.logtab

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.ui.MessageCategory
import nl.hannahsten.texifyidea.util.capitalizeFirst
import java.util.*

data class LatexLogMessage(val message: String, val fileName: String? = null, val line: Int = -1, val type: LatexLogMessageType = LatexLogMessageType.ERROR, val file: VirtualFile? = null) {

    fun toTreeViewString(): String {
        val typeString = type.toString().lowercase(Locale.getDefault()).capitalizeFirst()
        val lineString = if (line >= 0) "line ($line)" else ""
        return "$typeString:$lineString $message"
    }
}

enum class LatexLogMessageType(val category: Int) {

    ERROR(MessageCategory.ERROR),
    WARNING(MessageCategory.WARNING),
}

package nl.hannahsten.texifyidea.run.latex.logtab

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.DUPLICATE_WHITESPACE
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LINE_WIDTH
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.TEX_MISC_WARNINGS
import nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors.LatexErrorHandler
import nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors.LatexFixMeErrorMessageHandler
import nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors.LatexSingleLineErrorMessageHandler
import nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors.LatexUndefinedControlSequenceHandler
import nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings.*
import nl.hannahsten.texifyidea.util.remove

object LatexLogMessageExtractor {

    /**
     * Pre-processing to check if line is worth looking at.
     */
    fun skip(text: String?): Boolean {
        return text.isNullOrBlank()
    }

    /**
     * Look for a warning or error message in [text], and return a handler that
     * can handle the warning (i.e., process it and output the correct log message).
     * Return null if [text] does not contain such an error or warning.
     *
     * @param text Text in which to search for error messages (will be two consecutive lines)
     * @param newText Second line of 'text',
     */
    fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        val specialErrorHandlersList = listOf(
            LatexUndefinedControlSequenceHandler,
            LatexSingleLineErrorMessageHandler,
            LatexFixMeErrorMessageHandler
        )

        // Most of these are just to have a special regex to extract the right line number
        val specialWarningHandlersList = listOf(
            YouHaveRequestedOnInputLineVersionOfPackageWarningHandler,
            OverfullHboxWarningHandler,
            EndOccurredInsideGroupWarningHandler,
            EndOccurredWhenConditionWasIncompleteWarningHandler,
            LatexPackageWarningHandler,
            LatexReferenceCitationWarningHandler,
            LatexLineWarningHandler,
            LatexFixMeWarningMessageHandler,
            LatexPdftexWarningMessageHandler
        )
        // Problem: if we just joined the two lines together, we wouldn't know if the first line actually is part of (for example)
        // the filepath, and we might match on things like /home/.../package.sty\n/home/.../otherpackage.sty:42: Error Message
        // Proposal: only join the lines if the first line was broken by line length
        // Note that if it wasn't, we match the first line (not the second) because we will encounter the second line in the next iteration
        val textToMatch = if (text.removeSuffix(newText).length < LINE_WIDTH - 1) text.removeSuffix(newText) else text

        // Look for errors that need special treatment.
        specialErrorHandlersList.forEach { handler ->
            if (handler.regex.any { it.containsMatchIn(text) }) {
                return handler.findMessage(text, newText, currentFile)
            }
        }

        if (LatexErrorHandler.regex.any { it.containsMatchIn(textToMatch) }) {
            return LatexErrorHandler.findMessage(text, newText, currentFile)
        }

        // Look for warnings that need special treatment.
        specialWarningHandlersList.forEach { handler ->
            // Check if the match starts in 'text', because if not then we will encounter it again the next time
            if (handler.regex.any { r ->
                r.containsMatchIn(text) &&
                    r.find(text)?.range?.start?.let { it <= text.removeSuffix(newText).length - 1 } == true
            }
            ) {
                return handler.findMessage(text, newText, currentFile)
            }
        }

        // Check if we have found a warning
        val firstLine = text.removeSuffix(newText)
        if (TEX_MISC_WARNINGS.any { firstLine.startsWith(it) }) {
            // Don't include the second line if it is not part of the message
            // (Do this before cleaning up the messageText)
            var messageText = if (LatexOutputListener.isLineEndOfMessage(newText, firstLine)) firstLine.trim() else text

            messageText = messageText.remove("LaTeX Warning: ")
                // Improves readability, and at the moment we don't have an example where this would be incorrect
                .trim('(', ')', '[', ']')
                .replace(DUPLICATE_WHITESPACE.toRegex(), " ")

            return LatexLogMessage(
                messageText,
                fileName = currentFile,
                type = LatexLogMessageType.WARNING
            )
        }

        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab

import com.intellij.diagnostic.logging.AdditionalTabComponent
import com.intellij.execution.process.ProcessHandler
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.ui.LatexCompileMessageTreeView
import java.awt.BorderLayout
import javax.swing.JComponent

/**
 * Runner tab component displaying LaTeX log messages in a more readable and navigatable format.
 *
 * @param startedProcess The LaTeX compile process.
 *
 * @author Sten Wessel
 */
class LatexLogTabComponent(val project: Project, val mainFile: VirtualFile?, startedProcess: ProcessHandler) : AdditionalTabComponent(BorderLayout()) {

    private val latexMessageList = mutableListOf<LatexLogMessage>()

    // bibtex messages that need to be shown in the latex log tab (latexmk)
    private val bibtexMessageList = mutableListOf<BibtexLogMessage>()
    private val treeView = LatexCompileMessageTreeView(project, latexMessageList, bibtexMessageList)

    init {
        add(treeView, BorderLayout.CENTER)
        startedProcess.addProcessListener(LatexOutputListener(project, mainFile, latexMessageList, bibtexMessageList, treeView), this)
    }

    override fun getTabTitle() = "Log Messages"

    override fun dispose() {}

    override fun getPreferredFocusableComponent() = component

    override fun getToolbarActions(): ActionGroup? = null

    override fun getToolbarContextComponent(): JComponent? = null

    override fun getToolbarPlace(): String = "top"

    override fun getSearchComponent(): JComponent? = null

    override fun isContentBuiltIn() = false
}

package nl.hannahsten.texifyidea.run.latex.logtab

open class LatexMessageHandler(open val messageType: LatexLogMessageType, open vararg val regex: Regex) {

    open fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.forEach {
            it.find(text)?.apply {
                return LatexLogMessage(this.value, fileName = currentFile, type = messageType)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab

/**
 * A [LatexMessageHandler] but assuming that the last regex group is the source line number.
 */
open class LatexMessageWithLineHandler(override val messageType: LatexLogMessageType, override vararg val regex: Regex) : LatexMessageHandler(messageType, *regex) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.first().find(text)?.apply {
            val message = this.value
            val line = groupValues.drop(1).dropLastWhile { it.isBlank() }.lastOrNull()?.toInt() ?: 1
            return LatexLogMessage(
                message, currentFile, line,
                messageType
            )
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab

import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.process.ProcessOutputType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexOutputListener
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.DUPLICATE_WHITESPACE
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LINE_WIDTH
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.PACKAGE_WARNING_CONTINUATION
import nl.hannahsten.texifyidea.run.latex.logtab.ui.LatexCompileMessageTreeView
import nl.hannahsten.texifyidea.util.files.findFile
import nl.hannahsten.texifyidea.util.remove
import nl.hannahsten.texifyidea.util.removeAll
import org.apache.commons.collections.Buffer
import org.apache.commons.collections.BufferUtils
import org.apache.commons.collections.buffer.CircularFifoBuffer

class LatexOutputListener(
    val project: Project,
    val mainFile: VirtualFile?,
    private val messageList: MutableList<LatexLogMessage>,
    private val bibMessageList: MutableList<BibtexLogMessage>,
    val treeView: LatexCompileMessageTreeView
) : ProcessListener {

    // This should probably be located somewhere else
    companion object {

        /**
         * Returns true if firstLine is most likely the last line of the message.
         */
        fun isLineEndOfMessage(secondLine: String, firstLine: String): Boolean {
            return firstLine.remove("\n").length < LINE_WIDTH - 1 &&
                // Indent of LaTeX Warning/Error messages
                !secondLine.startsWith("               ") &&
                // Package warning/error continuation.
                !PACKAGE_WARNING_CONTINUATION.toRegex().containsMatchIn(secondLine) &&
                // Assume the undefined control sequence always continues on the next line
                !firstLine.trim().endsWith("Undefined control sequence.") &&
                // Case of the first line pointing out something interesting on the second line
                !(firstLine.endsWith(":") && secondLine.startsWith("    "))
        }
    }

    /**
     * Window of the last two log output messages.
     */
    val window: Buffer = BufferUtils.synchronizedBuffer(CircularFifoBuffer(2))

    // For latexmk, collect the bibtex/biber messages in a separate list, so
    // we don't lose them when resetting on each new (pdfla)tex run.
    private var isCollectingBib = false
    private val bibtexOutputListener = BibtexOutputListener(project, mainFile, bibMessageList, treeView)

    private var isCollectingMessage = false
    private var currentLogMessage: LatexLogMessage? = null

    // Stack with the filenames, where the first is the current file.
    private var fileStack = LatexFileStack()

    private var collectingOutputLine: String = ""

    override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
        if (outputType !is ProcessOutputType) return
        // Latexmk outputs on stderr, which interleaves with the pdflatex/bibtex/etc output on stdout
        if (outputType.isStderr) return

        // The following code wants complete lines as input.
        // However, this function onTextAvailable will simply give us text when it's available,
        // which may be only part of a line.
        // Since it does always include newlines to designate linebreaks, we simply wait here until
        // we have the full line collected.
        // It looks like we never get a line ending in the middle of even.text, but not completely sure about that.
        if (event.text.endsWith("\n").not()) {
            collectingOutputLine += event.text
            return
        }
        else {
            val collectedLine = collectingOutputLine + event.text
            collectingOutputLine = ""

            // We look for specific strings in the log output for which we know it was probably an error message
            // (it might be that it is actually not, since anything can be typed to the LaTeX output log, but we ignore
            // that for now and assume the log is of reasonable format).
            // Standard log output is only 79 characters wide, meaning these strings might occur over two lines.
            // We assume the match tokens will always fit over two lines.
            // Therefore, we maintain a window of two log output lines to detect the error messages.
            //
            // We assume that if the first line of the error/warning is exactly the maximum line width, it will continue
            // on the next line. This may not be accurate, but there is no way of distinguishing this.

            // Newlines are important to check when message end. Keep.
            processNewText(collectedLine)
        }
    }

    fun processNewText(newText: String) {
        if (isCollectingBib) {
            // Don't chunk bibtex lines, as they don't usually break by themselves and they are often not long anyway
            bibtexOutputListener.processNewText(newText)
        }
        else {
            newText.chunked(LINE_WIDTH).forEach {
                processNewTextLatex(it)
            }
        }
        resetIfNeeded(newText)
    }

    private fun processNewTextLatex(newText: String) {
        window.add(newText)
        // No idea how we could possibly get an IndexOutOfBoundsException on the buffer, but we did
        val text = try {
            window.joinToString(separator = "")
        }
        catch (e: IndexOutOfBoundsException) {
            return
        }

        // Check if we are currently in the process of collecting the full message of a matched message of interest
        if (isCollectingMessage) {
            collectMessageLine(text, newText)
            fileStack.update(newText)
        }
        else {
            // Skip line if it is irrelevant.
            if (LatexLogMessageExtractor.skip(window.firstOrNull() as? String)) {
                // The first line might be irrelevant, but the new text could
                // contain useful information about the file stack.
                fileStack.update(newText)
                return
            }

            // Find an error message or warning in the current text.
            val logMessage = LatexLogMessageExtractor.findMessage(text.removeAll("\n", "\r"), newText.removeAll("\n"), fileStack.peek())

            // Check for potential file opens/closes, modify the stack accordingly.
            fileStack.update(newText)

            // Finally add the message to the log, or continue collecting this message when necessary.
            addOrCollectMessage(text, newText, logMessage ?: return)
        }
    }

    private fun findProjectFileRelativeToMain(fileName: String?): VirtualFile? =
        mainFile?.parent?.findFile(fileName ?: mainFile.name, setOf("tex"))

    /**
     * Reset the tree view and the message list when starting a new run. (latexmk)
     */
    private fun resetIfNeeded(newText: String) {
        """Latexmk: applying rule '(?<program>\w+)""".toRegex().apply {
            val result = find(newText) ?: return@apply
            if (result.groups["program"]?.value in setOf("biber", "bibtex")) {
                isCollectingBib = true
            }
            else {
                isCollectingBib = false
                treeView.errorViewStructure.clear()
                messageList.clear()
                // Re-add the bib messages to the tree.
                bibMessageList.forEach { bibtexOutputListener.addBibMessageToTree(it) }
            }
        }
    }

    /**
     * Keep collecting the message if necessary, otherwise add it to the log.
     */
    private fun addOrCollectMessage(text: String, newText: String, logMessage: LatexLogMessage) {
        logMessage.apply {
            if (message.isEmpty()) return

            if (!isLineEndOfMessage(newText, logMessage.message) || text.removeSuffix(newText).length >= LINE_WIDTH) {
                // Keep on collecting output for this message
                currentLogMessage = logMessage
                isCollectingMessage = true
            }
            else {
                val file = findProjectFileRelativeToMain(fileName)

                if (messageList.isEmpty() || !messageList.contains(logMessage)) {
                    // Use original filename, especially for tests to work (which cannot find the real file)
                    // Trim message here instead of earlier in order to keep spaces in case we needed to continue
                    // collecting the message and the spaces were actually relevant
                    addMessageToLog(LatexLogMessage(message.trim(), fileName, line, type), file)
                }
            }
        }
    }

    /**
     * Add the current/new message to the log if it does not continue on the
     * next line.
     */
    private fun collectMessageLine(text: String, newText: String, logMessage: LatexLogMessage? = null) {
        // Check if newText is interesting before appending it to the message
        if (currentLogMessage?.message?.endsWith(newText.trim()) == false && !isLineEndOfMessage(newText, text.removeSuffix(newText))) {
            // Append new text
            val message = logMessage ?: currentLogMessage!!

            // Assume that lines that end prematurely do need an extra space to be inserted, like LaTeX and package
            // warnings with manual newlines, unlike 80-char forced linebreaks which should not have a space inserted
            val newTextTrimmed = if (text.removeSuffix(newText).length < LINE_WIDTH) " ${newText.trim()}" else newText.trim()

            // LaTeX Warning: is replaced here because this method is also run when a message is added,
            // and the above check needs to return false so we can't replace this in the WarningHandler
            var newMessage = (message.message + newTextTrimmed).replace("LaTeX Warning: ", "")
                .replace(PACKAGE_WARNING_CONTINUATION.toRegex(), "")
                .replace(DUPLICATE_WHITESPACE.toRegex(), " ")
                .replace(""". l.\d+ """.toRegex(), " ") // Continuation of Undefined control sequence

            // The 'on input line <line>' may be a lot of lines after the 'LaTeX Warning:', thus the original regex may
            // not have caught it. Try to catch the line number here.
            var line = message.line
            if (line == -1) {
                LatexLogMagicRegex.REPORTED_ON_LINE_REGEX.find(newMessage)?.apply {
                    line = groups["line"]?.value?.toInt() ?: -1
                    newMessage = newMessage.removeAll(this.value).trim()
                }
            }

            currentLogMessage = LatexLogMessage(newMessage, message.fileName, line, message.type)
        }
        else {
            isCollectingMessage = false
            addMessageToLog(currentLogMessage!!)
            currentLogMessage = null
        }
    }

    private fun addMessageToLog(logMessage: LatexLogMessage, givenFile: VirtualFile? = null) {
        // Don't log the same message twice
        if (!messageList.contains(logMessage)) {
            val file = givenFile ?: findProjectFileRelativeToMain(logMessage.fileName)
            val message = LatexLogMessage(logMessage.message.trim(), logMessage.fileName, logMessage.line, logMessage.type, file)
            messageList.add(message)
            treeView.applyFilters(message)
        }
    }

    override fun processTerminated(event: ProcessEvent) {
        if (event.exitCode == 0) {
            treeView.setProgressText("Compilation was successful.")
        }
        else {
            treeView.setProgressText("Compilation failed.")
        }
    }

    override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) {
    }

    override fun startNotified(event: ProcessEvent) {
        treeView.setProgressText("Compilation in progress...")
    }
}

package nl.hannahsten.texifyidea.run.latex.ui

import com.intellij.execution.configuration.EnvironmentVariablesComponent
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileTypeDescriptor
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.*
import com.intellij.openapi.util.SystemInfo
import com.intellij.ui.RawCommandLineEditor
import com.intellij.ui.SeparatorComponent
import com.intellij.ui.TitledSeparator
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfigurationType
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler.Format
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler.PDFLATEX
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.run.latex.LatexOutputPath
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.run.latex.externaltool.ExternalToolRunConfigurationType
import nl.hannahsten.texifyidea.run.linuxpdfviewer.InternalPdfViewer
import nl.hannahsten.texifyidea.run.makeindex.MakeindexRunConfigurationType
import nl.hannahsten.texifyidea.run.pdfviewer.ExternalPdfViewers
import nl.hannahsten.texifyidea.run.pdfviewer.PdfViewer
import nl.hannahsten.texifyidea.run.sumatra.SumatraAvailabilityChecker
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import java.awt.event.ItemEvent
import javax.swing.*

/**
 * @author Sten Wessel
 */
class LatexSettingsEditor(private var project: Project?) : SettingsEditor<LatexRunConfiguration>() {

    private lateinit var panel: JPanel
    private lateinit var compiler: LabeledComponent<ComboBox<LatexCompiler>>
    private lateinit var enableCompilerPath: JBCheckBox
    private lateinit var compilerPath: TextFieldWithBrowseButton
    private lateinit var compilerArguments: LabeledComponent<RawCommandLineEditor>
    private lateinit var environmentVariables: EnvironmentVariablesComponent
    private lateinit var mainFile: LabeledComponent<ComponentWithBrowseButton<*>>
    private lateinit var outputPath: LabeledComponent<ComponentWithBrowseButton<*>>

    // Not shown on non-MiKTeX systems
    private var auxilPath: LabeledComponent<ComponentWithBrowseButton<*>>? = null

    private var compileTwice: JBCheckBox? = null
    private lateinit var outputFormat: LabeledComponent<ComboBox<Format>>
    private lateinit var latexDistribution: LabeledComponent<ComboBox<LatexDistributionType>>
    private val extensionSeparator = TitledSeparator("Extensions")
    private lateinit var externalToolsPanel: RunConfigurationPanel

    /** Whether to enable the sumatraPath text field. */
    private lateinit var enableSumatraPath: JBCheckBox

    /** Allow users to specify a custom path to SumatraPDF.  */
    private lateinit var sumatraPath: TextFieldWithBrowseButton

    private lateinit var pdfViewer: LabeledComponent<ComboBox<out PdfViewer>>

    /** Whether to enable the custom pdf viewer command text field. */
    private lateinit var enableViewerCommand: JBCheckBox

    /** Allow users to specify a custom pdf viewer command. */
    private lateinit var viewerCommand: JBTextField

    // Discard all non-confirmed user changes made via the UI
    override fun resetEditorFrom(runConfiguration: LatexRunConfiguration) {
        // Reset the selected compiler.
        compiler.component.selectedItem = runConfiguration.compiler

        // Reset the custom compiler path
        compilerPath.text = runConfiguration.compilerPath ?: ""
        enableCompilerPath.isSelected = runConfiguration.compilerPath != null

        if (::sumatraPath.isInitialized) {
            // Reset the custom SumatraPDF path
            sumatraPath.text = runConfiguration.sumatraPath ?: ""
            enableSumatraPath.isSelected = runConfiguration.sumatraPath != null
        }

        pdfViewer.component.selectedItem = runConfiguration.pdfViewer

        // Reset the pdf viewer command
        viewerCommand.text = runConfiguration.viewerCommand ?: ""
        enableViewerCommand.isSelected = runConfiguration.viewerCommand != null

        // Reset compiler arguments
        val args = runConfiguration.compilerArguments
        compilerArguments.component.text = args ?: ""

        // Reset environment variables
        environmentVariables.envData = runConfiguration.environmentVariables

        // Reset the main file to compile.
        val txtFile = mainFile.component as TextFieldWithBrowseButton
        txtFile.text = runConfiguration.mainFile?.path ?: ""

        if (auxilPath != null) {
            val auxilPathTextField = auxilPath!!.component as TextFieldWithBrowseButton
            auxilPathTextField.text = runConfiguration.auxilPath.virtualFile?.path ?: runConfiguration.auxilPath.pathString
        }

        val outputPathTextField = outputPath.component as TextFieldWithBrowseButton
        // We may be editing a run configuration template, don't resolve any path
        outputPathTextField.text = runConfiguration.outputPath.virtualFile?.path ?: runConfiguration.outputPath.pathString

        // Reset whether to compile twice
        if (compileTwice != null) {
            if (runConfiguration.compiler?.handlesNumberOfCompiles == true) {
                compileTwice!!.isVisible = false
                runConfiguration.compileTwice = false
            }
            else {
                compileTwice!!.isVisible = true
            }
            compileTwice!!.isSelected = runConfiguration.compileTwice

            // If we need to compile twice, make sure the LatexCommandLineState knows
            if (runConfiguration.compileTwice) {
                runConfiguration.isLastRunConfig = false
            }
        }

        // Reset output format.
        // Make sure to use the output formats relevant for the chosen compiler
        if (runConfiguration.compiler != null) {
            outputFormat.component.removeAllItems()
            for (item in runConfiguration.compiler!!.outputFormats) {
                outputFormat.component.addItem(item)
            }
            if (runConfiguration.compiler!!.outputFormats.contains(runConfiguration.outputFormat)) {
                outputFormat.component.selectedItem = runConfiguration.outputFormat
            }
            else {
                outputFormat.component.selectedItem = Format.PDF
            }
        }

        // Reset LaTeX distribution
        latexDistribution.component.selectedItem = runConfiguration.latexDistribution

        // Reset project.
        project = runConfiguration.project

        // Reset run configs
        externalToolsPanel.configurations = runConfiguration.getAllAuxiliaryRunConfigs().toMutableSet()
    }

    // Confirm the changes, i.e. copy current UI state into the target settings object.
    @Throws(ConfigurationException::class)
    override fun applyEditorTo(runConfiguration: LatexRunConfiguration) {
        // Apply chosen compiler.
        val chosenCompiler = compiler.component.selectedItem as? LatexCompiler ?: PDFLATEX
        runConfiguration.compiler = chosenCompiler

        // Remove bibtex run config when switching to a compiler which includes running bibtex
        val includesBibtex = runConfiguration.compiler?.includesBibtex == true
        val includesMakeindex = runConfiguration.compiler?.includesMakeindex == true
        if (includesBibtex || includesMakeindex) {
            if (includesBibtex) {
                runConfiguration.bibRunConfigs = setOf()
            }
            if (includesMakeindex) {
                runConfiguration.makeindexRunConfigs = setOf()
            }
            // Panel remains visible, to allow adding ExternalToolRunConfiguration
        }
        else {
            // Update run config based on UI
            runConfiguration.bibRunConfigs = externalToolsPanel.configurations.filter { it.type is BibtexRunConfigurationType }.toSet()
            runConfiguration.makeindexRunConfigs = externalToolsPanel.configurations.filter { it.type is MakeindexRunConfigurationType }.toSet()
            runConfiguration.externalToolRunConfigs = externalToolsPanel.configurations.filter { it.type is ExternalToolRunConfigurationType }.toSet()
        }

        // Apply custom compiler path if applicable
        runConfiguration.compilerPath = if (enableCompilerPath.isSelected) compilerPath.text else null

        if (::sumatraPath.isInitialized) {
            // Apply custom SumatraPDF path if applicable
            runConfiguration.sumatraPath = if (enableSumatraPath.isSelected) sumatraPath.text else null

            runConfiguration.enableSumatraPath = enableSumatraPath.isSelected
        }

        runConfiguration.pdfViewer = pdfViewer.component.selectedItem as? PdfViewer ?: InternalPdfViewer.firstAvailable()

        // Apply custom pdf viewer command
        runConfiguration.viewerCommand = if (enableViewerCommand.isSelected) viewerCommand.text else null

        // Apply custom compiler arguments
        runConfiguration.compilerArguments = compilerArguments.component.text

        // Apply environment variables
        runConfiguration.environmentVariables = environmentVariables.envData

        // Apply main file.
        val txtFile = mainFile.component as TextFieldWithBrowseButton
        val filePath = txtFile.text
        runConfiguration.setMainFile(filePath)

        val outputPathTextField = outputPath.component as TextFieldWithBrowseButton
        if (!outputPathTextField.text.endsWith("/bin")) {
            runConfiguration.setFileOutputPath(outputPathTextField.text)
        }

        if (auxilPath != null) {
            val auxilPathTextField = auxilPath!!.component as TextFieldWithBrowseButton
            runConfiguration.setFileAuxilPath(auxilPathTextField.text)
        }

        if (compileTwice != null) {
            // Only show option to configure number of compiles when applicable
            if (runConfiguration.compiler?.handlesNumberOfCompiles == true) {
                compileTwice!!.isVisible = false
                runConfiguration.compileTwice = false
            }
            else {
                compileTwice!!.isVisible = true
                runConfiguration.compileTwice = compileTwice!!.isSelected
            }

            // If we need to compile twice, make sure the LatexCommandLineState knows
            if (runConfiguration.compileTwice) {
                runConfiguration.isLastRunConfig = false
            }
        }

        // Apply output format.
        val format = outputFormat.component.selectedItem as Format?
        runConfiguration.outputFormat = format ?: Format.PDF

        // Apply LaTeX distribution
        runConfiguration.latexDistribution = latexDistribution.component.selectedItem as LatexDistributionType? ?: LatexDistributionType.TEXLIVE

        if (chosenCompiler == LatexCompiler.ARARA) {
            outputPath.isVisible = false
            auxilPath?.isVisible = false
            outputFormat.isVisible = false
        }
        else {
            outputPath.isVisible = true
            auxilPath?.isVisible = true
            outputFormat.isVisible = true
        }
    }

    override fun createEditor(): JComponent {
        createUIComponents()
        return panel
    }

    private fun createUIComponents() {
        // Layout
        panel = JPanel()
        panel.layout = VerticalFlowLayout(VerticalFlowLayout.TOP)

        addCompilerPathField(panel)

        addSumatraPathField(panel)

        addPdfViewerCommandField(panel)

        // Optional custom compiler arguments
        val argumentsTitle = "Custom compiler arguments"
        val argumentsField = RawCommandLineEditor()

        compilerArguments = LabeledComponent.create(argumentsField, argumentsTitle)
        panel.add(compilerArguments)

        environmentVariables = EnvironmentVariablesComponent()
        panel.add(environmentVariables)

        panel.add(SeparatorComponent())

        // Main file selection
        val mainFileField = TextFieldWithBrowseButton()
        mainFileField.addBrowseFolderListener(
            TextBrowseFolderListener(
                FileTypeDescriptor("Choose a File to Compile", ".tex")
                    .withRoots(
                        *ProjectRootManager.getInstance(project!!)
                            .contentRootsFromAllModules
                    )
            )
        )
        mainFile = LabeledComponent.create(mainFileField, "Main file to compile")
        panel.add(mainFile)

        addOutputPathField(panel)

        compileTwice = JBCheckBox("Always compile at least twice")
        compileTwice!!.isSelected = false
        panel.add(compileTwice)

        // Output format.
        val selectedCompiler = compiler.component.selectedItem as LatexCompiler
        val cboxFormat = ComboBox(selectedCompiler.outputFormats)
        outputFormat = LabeledComponent.create(cboxFormat, "Output format")
        outputFormat.setSize(128, outputFormat.height)
        panel.add(outputFormat)

        // LaTeX distribution
        @Suppress("DialogTitleCapitalization")
        latexDistribution = LabeledComponent.create(ComboBox(LatexDistributionType.values().filter { it.isAvailable(project!!) }.toTypedArray()), "LaTeX Distribution")
        panel.add(latexDistribution)

        panel.add(extensionSeparator)

        // Extension panel
        externalToolsPanel = RunConfigurationPanel(project!!, "External LaTeX programs: ")
        panel.add(externalToolsPanel)
    }

    private fun addOutputPathField(panel: JPanel) {
        // The aux directory is only available on MiKTeX, so only allow disabling on MiKTeX
        if (LatexSdkUtil.isMiktexAvailable) {
            val auxilPathField = TextFieldWithBrowseButton()
            auxilPathField.addBrowseFolderListener(
                TextBrowseFolderListener(
                    FileChooserDescriptor(false, true, false, false, false, false)
                        .withTitle("Auxiliary Files Directory")
                        .withRoots(
                            *ProjectRootManager.getInstance(project!!)
                                .contentRootsFromAllModules
                        )
                )
            )
            auxilPath = LabeledComponent.create(auxilPathField, "Directory for auxiliary files")
            panel.add(auxilPath)
        }

        val outputPathField = TextFieldWithBrowseButton()
        outputPathField.addBrowseFolderListener(
            TextBrowseFolderListener(
                FileChooserDescriptor(false, true, false, false, false, false)
                    .withTitle("Output Files Directory")
                    .withRoots(
                        *ProjectRootManager.getInstance(project!!)
                            .contentRootsFromAllModules
                    )
            )
        )
        outputPath = LabeledComponent.create(outputPathField, "Directory for output files, you can use ${LatexOutputPath.mainFileString} or ${LatexOutputPath.projectDirString} as placeholders:")
        panel.add(outputPath)
    }

    /**
     * Compiler with optional custom path for compiler executable.
     */
    private fun addCompilerPathField(panel: JPanel) {
        // Compiler
        val compilerField = ComboBox(LatexCompiler.values())
        compiler = LabeledComponent.create(compilerField, "Compiler")
        panel.add(compiler)

        enableCompilerPath = JBCheckBox("Select custom compiler executable path (required on Mac OS X)")
        panel.add(enableCompilerPath)

        compilerPath = TextFieldWithBrowseButton()
        compilerPath.addBrowseFolderListener(
            TextBrowseFolderListener(
                FileChooserDescriptor(true, false, false, false, false, false)
                    .withFileFilter { virtualFile -> virtualFile.nameWithoutExtension == (compilerField.selectedItem as LatexCompiler).executableName }
                    .withTitle("Choose " + compilerField.selectedItem + " executable")
            )
        )
        compilerPath.isEnabled = false
        compilerPath.addPropertyChangeListener("enabled") { e ->
            if (!(e.newValue as Boolean)) {
                compilerPath.setText(null)
            }
        }
        enableCompilerPath.addItemListener { e -> compilerPath.isEnabled = e.stateChange == ItemEvent.SELECTED }

        panel.add(compilerPath)
    }

    private fun updatePdfViewerComboBox() {
        val viewers = InternalPdfViewer.availableSubset().filter { it != InternalPdfViewer.NONE } +
            ExternalPdfViewers.getExternalPdfViewers() +
            listOf(InternalPdfViewer.NONE)

        pdfViewer.component.removeAllItems()
        for (i in viewers.indices) {
            @Suppress("UNCHECKED_CAST")
            (pdfViewer.component as ComboBox<PdfViewer>).addItem(viewers[i])
        }
        pdfViewer.updateUI()
    }

    /**
     * Optional custom path for SumatraPDF.
     */
    private fun addSumatraPathField(panel: JPanel) {
        class PathInputVerifier : InputVerifier() {

            override fun verify(input: JComponent?): Boolean {
                updatePdfViewerComboBox()
                return true
            }
        }

        if (SystemInfo.isWindows) {
            enableSumatraPath = JBCheckBox("Select custom path to SumatraPDF")
            panel.add(enableSumatraPath)

            sumatraPath = TextFieldWithBrowseButton().apply {
                @Suppress("DialogTitleCapitalization")
                addBrowseFolderListener(
                    TextBrowseFolderListener(
                        FileChooserDescriptor(false, true, false, false, false, false)
                            .withTitle("SumatraPDF.exe Location")
                    )
                )

                isEnabled = false

                addPropertyChangeListener("enabled") { e ->
                    if (!(e.newValue as Boolean)) {
                        this.setText(null)
                    }
                }
            }

            sumatraPath.textField.inputVerifier = PathInputVerifier()

            enableSumatraPath.addItemListener { e ->
                if (e.stateChange != ItemEvent.SELECTED) {
                    // Removes the custom Sumatra path from SumatraAvailabilityChecker when unchecked
                    SumatraAvailabilityChecker.isSumatraPathAvailable(null)
                    updatePdfViewerComboBox()
                }
                sumatraPath.isEnabled = e.stateChange == ItemEvent.SELECTED
            }

            panel.add(sumatraPath)
        }
    }

    /**
     * Optional custom pdf viewer command text field.
     */
    private fun addPdfViewerCommandField(panel: JPanel) {
        val viewers = InternalPdfViewer.availableSubset().filter { it != InternalPdfViewer.NONE } +
            ExternalPdfViewers.getExternalPdfViewers() +
            listOf(InternalPdfViewer.NONE)

        val viewerField = ComboBox(viewers.toTypedArray())
        pdfViewer = LabeledComponent.create(viewerField, "PDF viewer")
        panel.add(pdfViewer)

        enableViewerCommand = JBCheckBox("Select custom PDF viewer command, using {pdf} for the pdf file if not the last argument")
        panel.add(enableViewerCommand)

        viewerCommand = JBTextField().apply {
            isEnabled = false
            addPropertyChangeListener("enabled") { e ->
                if (!(e.newValue as Boolean)) {
                    this.text = null
                }
            }
        }

        enableViewerCommand.addItemListener { e -> viewerCommand.isEnabled = e.stateChange == ItemEvent.SELECTED }

        panel.add(viewerCommand)
    }
}

package nl.hannahsten.texifyidea.run.latex.ui

import com.intellij.execution.RunManagerEx
import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.openapi.project.Project
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.SimpleTextAttributes
import javax.swing.JList

/**
 * @author Sten Wessel
 */
class RunConfigCellRenderer(private val project: Project) : ColoredListCellRenderer<RunnerAndConfigurationSettings>() {

    override fun customizeCellRenderer(
        list: JList<out RunnerAndConfigurationSettings>,
        settings: RunnerAndConfigurationSettings?, index: Int,
        selected: Boolean, hasFocus: Boolean
    ) {
        icon = RunManagerEx.getInstanceEx(project).getConfigurationIcon(settings ?: return)
        append(
            settings.configuration.name,
            if (settings.isTemporary) SimpleTextAttributes.GRAY_ATTRIBUTES
            else SimpleTextAttributes.REGULAR_ATTRIBUTES
        )
        append(" " + settings.configuration.type.displayName, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
    }
}

package nl.hannahsten.texifyidea.run.latex.ui

import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.openapi.actionSystem.ActionToolbarPosition
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBList
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.CollapsibleRow
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.ui.JBUI
import nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfigurationType
import nl.hannahsten.texifyidea.run.latex.externaltool.ExternalToolRunConfigurationType
import nl.hannahsten.texifyidea.run.makeindex.MakeindexRunConfigurationType
import java.awt.BorderLayout
import javax.swing.DefaultListSelectionModel
import javax.swing.JPanel

/**
 * @author Sten Wessel
 */
class RunConfigurationPanel(
    private val project: Project,
    private val title: String,
) : JPanel(BorderLayout()) {

    private val contentPanel = JPanel(BorderLayout())
    private val hidePanel: DialogPanel
    private var panelGroup: CollapsibleRow? = null
    private lateinit var list: JBList<RunnerAndConfigurationSettings>

    var configurations: MutableSet<RunnerAndConfigurationSettings> = mutableSetOf()
        set(value) {
            field = value
            configurationChanged()
        }

    init {

        createPanel()

        hidePanel = panel {
            collapsibleGroup(title) {
                row {
                    cell(contentPanel)
                        .align(AlignX.FILL)
                }
            }.also {
                panelGroup = it
            }
        }
        add(hidePanel, BorderLayout.CENTER)
    }

    private fun createPanel() {
        list = JBList<RunnerAndConfigurationSettings>().apply {
            emptyText.text = "No run configurations selected."
            cellRenderer = RunConfigCellRenderer(project)

            // Cell height
            prototypeCellValue = RunManagerImpl.getInstanceImpl(project).allSettings.firstOrNull()

            selectionModel = DefaultListSelectionModel()
        }

        val toolbar = ToolbarDecorator.createDecorator(list).apply {
            setToolbarPosition(ActionToolbarPosition.TOP)
            setPanelBorder(JBUI.Borders.empty())

            // No support for executing run configs in a certain order (yet)
            disableUpDownActions()

            setAddAction {
                configurations.addAll(askRunConfigurations())
                configurationChanged()
            }

            setRemoveAction {
                configurations.removeAll(list.selectedValuesList.toSet())
                configurationChanged()
            }
        }

        contentPanel.add(toolbar.createPanel(), BorderLayout.CENTER)
    }

    private fun askRunConfigurations(): List<RunnerAndConfigurationSettings> {
        val types = setOf(BibtexRunConfigurationType::class.java, MakeindexRunConfigurationType::class.java, ExternalToolRunConfigurationType::class.java)
        val configurations = RunManagerImpl.getInstanceImpl(project).allSettings.filter { it.type.javaClass in types }

        val dialog = RunConfigurationSelectionDialog(project, configurations)

        dialog.show()

        return dialog.selected
    }

    private fun changeTitle(titleSuffix: String) {
        panelGroup?.setTitle(title + titleSuffix)
    }

    private fun configurationChanged() {
        if (configurations.isNotEmpty()) {
            list.setListData(configurations.map { it }.toTypedArray())
            // Set cell height based on the size of the content
            list.visibleRowCount = configurations.size

            // Mock value change to commit changes (otherwise the apply button is not activated)
            list.setSelectionInterval(0, 0)
            changeTitle("Enabled")
            return
        }

        list.setListData(emptyArray())
        list.visibleRowCount = 1

        // Mock value change to commit changes (otherwise the apply button is not activated)
        list.setSelectionInterval(-1, -1)

        changeTitle("Disabled")
    }
}

package nl.hannahsten.texifyidea.run.latex.ui

import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.components.JBList
import com.intellij.ui.components.JBScrollPane
import java.awt.Dimension
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JComponent
import javax.swing.ListSelectionModel
import javax.swing.SwingUtilities

/**
 * Adapted from [com.intellij.execution.impl.RunConfigurationBeforeRunProvider]
 *
 * @author Sten Wessel
 */
class RunConfigurationSelectionDialog(
    private val project: Project,
    private val settings: List<RunnerAndConfigurationSettings>,
    selected: List<RunnerAndConfigurationSettings> = emptyList()
) : DialogWrapper(project) {

    private lateinit var list: JBList<RunnerAndConfigurationSettings>

    var selected = selected
        get() = if (isOK) field else emptyList()

    init {
        title = "Choose Run Configuration"
        init()

        list.apply {
            setSelectedValue(selected, true)

            addMouseListener(object : MouseAdapter() {
                override fun mouseClicked(e: MouseEvent?) {
                    if (SwingUtilities.isLeftMouseButton(e) && e?.clickCount == 2) {
                        doOKAction()
                    }
                }
            })

            val fontMetrics = list.getFontMetrics(list.font)

            // Icon and gap (= 24) + max size of the configuration names
            val maxWidth = 24 + (
                settings.asSequence().map { fontMetrics.stringWidth(it.configuration.name) }.maxOrNull()
                    ?: (fontMetrics.stringWidth("m") * 30)
                )

            minimumSize = Dimension(maxWidth, preferredSize.height)
        }
    }

    override fun createCenterPanel(): JComponent {
        list = JBList(settings).apply {
            selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION

            selectionModel.addListSelectionListener {
                selected = list.selectedValuesList
                isOKActionEnabled = !list.selectedValuesList.isNullOrEmpty()
            }

            cellRenderer = RunConfigCellRenderer(project)
        }

        return JBScrollPane(list)
    }
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer.evince

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.run.linuxpdfviewer.ViewerConversation
import nl.hannahsten.texifyidea.util.runCommand
import org.freedesktop.dbus.connections.impl.DBusConnection
import org.freedesktop.dbus.errors.NoReply
import org.freedesktop.dbus.errors.ServiceUnknown
import org.gnome.evince.Daemon

/**
 * Send commands to Evince.
 * For more information about D-Bus and forward/inverse search, see https://github.com/PHPirates/evince_dbus
 *
 * @author Thomas Schouten
 */
object EvinceConversation : ViewerConversation() {

    /**
     * Object path of the Evince daemon. Together with the object name, this allows us to find the
     * D-Bus object which allows us to execute the FindDocument function, which is exported on the D-Bus
     * by Evince.
     */
    private const val evinceDaemonPath = "/org/gnome/evince/Daemon"

    /**
     * Object name of the Evince daemon.
     */
    private const val evinceDaemonName = "org.gnome.evince.Daemon"

    /**
     * This variable will hold the latest known Evince process owner. We need to know the owner of the pdf file in order to execute forward search.
     */
    private var processOwner: String? = null

    /**
     * Open a file in Evince, starting it if it is not running yet. This also finds the process owner of the pdf, so we can execute forward search later.
     */
    fun openFile(pdfFilePath: String, project: Project) {
        // Will do nothing if file is already open in Evince
        findProcessOwner(pdfFilePath, project)
    }

    /**
     * Execute forward search, highlighting a certain line in Evince.
     * If a pdf file is given, it will execute FindDocument and open the pdf file again to find the latest process owner. If the pdf file is already open, this will do nothing.
     *
     * @param pdfPath Full path to a pdf file.
     * @param sourceFilePath Full path to the LaTeX source file.
     * @param line Line number in the source file to highlight in the pdf.
     */
    override fun forwardSearch(pdfPath: String?, sourceFilePath: String, line: Int, project: Project, focusAllowed: Boolean) {
        // If we are not allowed to change focus, we cannot open the pdf or do forward search because this will always change focus with Evince
        if (!focusAllowed) {
            return
        }

        if (pdfPath != null) {
            findProcessOwner(pdfPath, project)
        }

        if (processOwner != null) {
            // Theoretically we should use the Java D-Bus bindings as well to call SyncView, but that did
            // not succeed with a NoReply exception, so we will execute a command via the shell
            val command = "gdbus call --session --dest $processOwner --object-path /org/gnome/evince/Window/0 --method org.gnome.evince.Window.SyncView $sourceFilePath '($line, 1)' 0"
            runCommand("bash", "-c", command)
        }
        else {
            // If the user used the forward search menu action
            if (pdfPath == null) {
                Notification("LaTeX", "Could not execute forward search", "Please make sure you have compiled the document first, and that your path does not contain spaces.", NotificationType.ERROR).notify(project)
            }
            else {
                throw TeXception("Could not execute forward search with Evince because something went wrong when finding the pdf file at $pdfPath")
            }
        }
    }

    /**
     * Execute FindDocument on the D-Bus in order to find the process owner of the given pdf file, i.e. the process name which we can use for
     * forward/inverse search.
     * The value found will be saved for later use.
     *
     * @param pdfFilePath Full path to the pdf file to find the owner of.
     */
    private fun findProcessOwner(pdfFilePath: String, project: Project) {
        // Initialize a session bus
        val connection = DBusConnection.getConnection(DBusConnection.DBusBusType.SESSION)

        // Get the Daemon object using its bus name and object path
        val daemon = connection.getRemoteObject(evinceDaemonName, evinceDaemonPath, Daemon::class.java)

        // Call the method on the D-Bus by using the function we defined in the Daemon interface
        // Catch a NoReply, because it is unknown why Evince cannot start so we don't try to fix that
        try {
            processOwner = daemon.FindDocument("file://$pdfFilePath", true)
        }
        catch (ignored: NoReply) {}
        catch (e: ServiceUnknown) {
            Notification("LaTeX", "Cannot communicate to Evince", "Please update Evince and then try again.", NotificationType.ERROR).notify(project)
        }
    }
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer.evince

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.project.Project
import kotlinx.coroutines.*
import nl.hannahsten.texifyidea.util.SystemEnvironment
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader

/**
 * Listen on the D-Bus for inverse search signals coming from Evince.
 *
 * For debugging, the self-contained project at https://github.com/PHPirates/evince_dbus may be helpful.
 *
 * @author Thomas Schouten
 */
object EvinceInverseSearchListener {

    private var currentCoroutineScope = CoroutineScope(Dispatchers.Default)

    /**
     * Starts a listener which listens for inverse search actions from Evince.
     */
    fun start(project: Project) {
        // Check if Evince version supports dbus
        // The exact version required is not know, but 3.28 works and 3.0.2 does not (#2087), even though dbus is supported since 2.32
        if (SystemEnvironment.evinceVersion.majorVersion <= 3 && SystemEnvironment.evinceVersion.minorVersion <= 28) {
            Notification("LaTeX", "Old Evince version found", "Please update Evince to at least version 3.28 to use forward/backward search", NotificationType.ERROR).notify(project)
            return
        }

        // Run in a coroutine so the main thread can continue
        // If the program finishes, the listener will stop as well
        currentCoroutineScope.launch {
            startListening()
        }
    }

    /**
     * Start listening for backward search calls on the D-Bus.
     */
    private fun startListening() {
        try {
            // Listen on the session D-Bus to catch SyncSource signals emitted by Evince
            val commands = arrayOf("dbus-monitor", "--session")
            val process = Runtime.getRuntime().exec(commands)
            val bufferedReader = BufferedReader(InputStreamReader(process.inputStream))
            var line: String? = bufferedReader.readLine()

            while (line != null && currentCoroutineScope.isActive) {
                // Check if a SyncSource signal appeared from Evince and if so, read the contents
                if (line.contains("interface=org.gnome.evince.Window; member=SyncSource")) {
                    // Get the value between quotes
                    val filenameLine = bufferedReader.readLine()
                    var filename = filenameLine.substring(filenameLine.indexOf("\"") + 1, filenameLine.lastIndexOf("\""))
                    filename = filename.replaceFirst("file://".toRegex(), "")

                    // Pass over the "struct {" line
                    bufferedReader.readLine()

                    // Get the location represented by the struct
                    val lineNumberLine = bufferedReader.readLine()
                    val lineNumberString = lineNumberLine.substring(lineNumberLine.indexOf("int32") + 6, lineNumberLine.length).trim()
                    val lineNumber = Integer.parseInt(lineNumberString)

                    // Sync the IDE
                    syncSource(filename, lineNumber)
                }

                // Check whether we would block before doing a blocking readLine call
                // This is to ensure we can quickly stop this coroutine on plugin unload
                while (!bufferedReader.ready()) {
                    Thread.sleep(100)
                    if (!currentCoroutineScope.isActive) return
                }
                line = bufferedReader.readLine()
            }
        }
        catch (e: IOException) {
            e.printStackTrace()
        }
    }

    /**
     * Sync the IDE on the given source file and line number.
     *
     * @param filePath Full to a file.
     * @param lineNumber Line number in the file.
     */
    private fun syncSource(filePath: String, lineNumber: Int) {
        val path = PathManager.getBinPath()
        val name = ApplicationNamesInfo.getInstance().scriptName

        val command = "$path/$name.sh --line $lineNumber \"$filePath\""

        try {
            Runtime.getRuntime().exec(command)
        }
        catch (e: IOException) {
            e.printStackTrace()
        }
    }

    fun unload() {
        currentCoroutineScope.cancel(CancellationException(("Unloading the plugin")))
    }
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer.okular

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.run.linuxpdfviewer.ViewerConversation

/**
 * Execute Okular commands.
 *
 * @author Abby Berkers
 */
object OkularConversation : ViewerConversation() {

    private var pdfFilePath: String? = null

    /**
     * Execute a forward search, opens the pdf file in okular with the line that corresponds to the cursor roughly in the center.
     * Unfortunately this line does not get highlighted.
     *
     * @param pdfPath Full path of the pdf.
     * @param sourceFilePath Full path of the tex file.
     * @param line Line number in the source file to navigate to in the pdf.
     */
    override fun forwardSearch(pdfPath: String?, sourceFilePath: String, line: Int, project: Project, focusAllowed: Boolean) {
        if (pdfPath != null) {
            pdfFilePath = pdfPath
        }

        if (pdfFilePath != null) {
            // This okular command opens the pdf file using the destination coming from the line in the tex file.
            val command = "okular --noraise --unique '$pdfFilePath#src:$line $sourceFilePath'"
            Runtime.getRuntime().exec(arrayOf("bash", "-c", command))
        }
        else {
            Notification("LaTeX", "Could not execute forward search", "Please make sure you have compiled the document first.", NotificationType.ERROR).notify(project)
        }
    }
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer.skim

import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.run.linuxpdfviewer.ViewerConversation

/**
 * Execute Skim commands.
 *
 * @author Stephan Sundermann
 */
object SkimConversation : ViewerConversation() {

    private var pdfFilePath: String? = null

    /**
     * Execute a forward search, opens the pdf file in Skim with the line that corresponds to the cursor roughly in the center.
     *
     * @param pdfPath Full path of the pdf.
     * @param sourceFilePath Full path of the tex file.
     * @param line Line number in the source file to navigate to in the pdf.
     */
    override fun forwardSearch(pdfPath: String?, sourceFilePath: String, line: Int, project: Project, focusAllowed: Boolean) {
        val backgroundParameter = if (focusAllowed) "" else "-g"

        if (pdfPath != null) {
            pdfFilePath = pdfPath
        }

        if (pdfFilePath != null) {
            // This command opens the pdf file using the destination coming from the line in the tex file.
            val command = "/Applications/Skim.app/Contents/SharedSupport/displayline $backgroundParameter -r $line '$pdfFilePath' '$sourceFilePath'"
            Runtime.getRuntime().exec(arrayOf("bash", "-c", command))
        }
        else {
            Notification("LaTeX", "Could not execute forward search", "Please make sure you have compiled the document first.", NotificationType.ERROR).notify(project)
        }
    }
}

package nl.hannahsten.texifyidea.run.linuxpdfviewer.zathura

import com.intellij.execution.RunManager
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiManager
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.run.linuxpdfviewer.ViewerConversation
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import nl.hannahsten.texifyidea.util.selectedRunConfig

object ZathuraConversation : ViewerConversation() {

    override fun forwardSearch(pdfPath: String?, sourceFilePath: String, line: Int, project: Project, focusAllowed: Boolean) {
        val pdfPathGuess = pdfPath ?: guessPdfPath(project, sourceFilePath)

        if (pdfPathGuess != null) {
            val path = PathManager.getBinPath()
            val name = ApplicationNamesInfo.getInstance().scriptName
            val command =
                """zathura --synctex-forward="$line:1:$sourceFilePath" --synctex-editor-command="$path/$name.sh --line %{line} %{input}" $pdfPathGuess"""
            Runtime.getRuntime().exec(arrayOf("bash", "-c", command))
        }
        else {
            Notification("LaTeX", "Could not execute forward search", "Please make sure you have compiled the document first.", NotificationType.ERROR).notify(project)
        }
    }

    /**
     * Guess the path of the pdf file to forward search to based on the currently selected run configuration (as this is
     * often the last configuration that is run).
     */
    private fun guessPdfPath(project: Project, sourceFilePath: String): String? {
        val sourceVirtualFile = VirtualFileManager.getInstance().findFileByUrl("file://$sourceFilePath") ?: return null
        val sourcePsiFile = PsiManager.getInstance(project).findFile(sourceVirtualFile) ?: return null

        // First check if the file in the editor (sourceFilePath) is in the file set of the main file of the latest run run configuration.
        // If so, guess the main file (pdf) of that run config as the pdf.
        val runConfig = project.selectedRunConfig() ?: return null
        return if (runConfig.mainFile?.psiFile(project)?.referencedFileSet()?.contains(sourcePsiFile) == true) {
            // outputFilePath contains the file name and pdf extension (already). We don't have to add it.
            runConfig.outputFilePath
        }
        // If not, search for a run configuration that compiles the root file of the current file,
        // and use the output path that is specified there.
        else {
            runConfigThatCompilesFile(sourceVirtualFile, project)?.outputFilePath ?: return null
        }
    }

    /**
     * Get the run config that compiles the virtualFile, i.e.,
     * the run configuration that has [virtualFile] as its main file.
     */
    private fun runConfigThatCompilesFile(virtualFile: VirtualFile, project: Project): LatexRunConfiguration? =
        (RunManagerImpl.getInstanceImpl(project) as RunManager)
            .allConfigurationsList
            .filterIsInstance<LatexRunConfiguration>()
            .firstOrNull { it.mainFile == virtualFile }
}

package nl.hannahsten.texifyidea.ui.symbols.tools

import nl.hannahsten.texifyidea.ui.symbols.SymbolCategories
import nl.hannahsten.texifyidea.ui.symbols.SymbolUiEntry
import nl.hannahsten.texifyidea.util.encloseWhen
import java.io.File
import java.util.concurrent.TimeUnit

/**
 * 220 -> 44x44 pixels.
 */
private const val DENSITY = 220
private const val QUALITY = 100

/**
 * Generates the list of all symbols for the symbol view based on the [SymbolCategories.symbolList] list.
 *
 * Requires the following programs to be installed: pdflatex, ImageMagick, and Ghostscript.
 *
 * @author Hannah Schellekens
 */
fun generateSymbolImages(symbolDirectory: String, skipExisting: Boolean = true) = auxDirectory(symbolDirectory) {
    val symbols = SymbolCategories.symbolList.distinctBy { it.imagePath }

    symbols.forEach { symbol ->
        println("Generating images for symbol " + symbol.command?.commandWithSlash)

        if (skipExisting && File("$symbolDirectory/${symbol.fileName}").exists() &&
            File("$symbolDirectory/${symbol.fileName}").exists()
        ) {
            println("> Skipping")
            return@forEach
        }

        symbol.generateImages(symbolDirectory)
    }
}

/**
 * Generates the images for the symbol.
 */
private fun SymbolUiEntry.generateImages(symbolDirectory: String) {
    createLatexFile(this, Theme.LIGHT, symbolDirectory)
    convertToImage(this, Theme.LIGHT, symbolDirectory)
    File("$symbolDirectory/texify-symbol.tex").delete()

    createLatexFile(this, Theme.DARK, symbolDirectory)
    convertToImage(this, Theme.DARK, symbolDirectory)
    File("$symbolDirectory/texify-symbol.tex").delete()
}

/**
 * Converts the tex file to a png image.
 */
private fun convertToImage(
    symbol: SymbolUiEntry,
    theme: Theme,
    symbolDirectory: String,
    latexFileName: String = "texify-symbol.tex",
    auxilDirectory: String = "auxil"
) {
    // Create pdf.
    ProcessBuilder(
        "pdflatex", "-job-name=texify-symbol", "-aux-directory=\"$auxilDirectory\"", latexFileName
    ).directory(File(symbolDirectory)).start().waitFor(6, TimeUnit.SECONDS)

    // Convert pdf to image.
    val imageName = if (theme == Theme.DARK) symbol.fileNameDark else symbol.fileName
    ProcessBuilder(
        "magick", "convert", "-density", DENSITY.toString(), "-quality", QUALITY.toString(), "texify-symbol.pdf",
        imageName
    ).directory(File(symbolDirectory)).start().waitFor(6, TimeUnit.SECONDS)

    // Delete leftover pdf.
    File("$symbolDirectory/texify-symbol.pdf").delete()
}

/**
 * Creates the tex file to compile.
 */
private fun createLatexFile(symbol: SymbolUiEntry, theme: Theme, directory: String, fileName: String = "texify-symbol.tex") {
    val result = File("$directory/$fileName")
    val latex = latex(symbol, theme)
    result.writeText(latex)
}

/**
 * Generates the latex used to generate the image.
 */
private fun latex(symbol: SymbolUiEntry, theme: Theme): String {
    val latex = symbol.imageLatex.encloseWhen(prefix = "$\\displaystyle ", suffix = "$") { symbol.isMathSymbol }
    val dependency = if (symbol.dependency.isDefault) "" else "\\usepackage{${symbol.dependency.name}}\n"

    return """%! author = Sten Wessel
    |\documentclass{standalone}
    |\usepackage{xcolor}
    |\usepackage{adjustbox}
    |\usepackage{amsmath}$dependency
    |\definecolor{${Theme.LIGHT.colourName}}{HTML}{000000}
    |\definecolor{${Theme.DARK.colourName}}{HTML}{BBBBBB}
    |\begin{document}
    |\color{$theme}\parbox[c][3.33ex][c]{3.33ex}{\centering\maxsizebox*{!}{2.5ex}{\maxsizebox*{2.5ex}{!}{$latex}}}
    |\end{document}
    """.trimMargin()
}

/**
 * Creates an aux directory if it does not yet exists and deletes it afterwards.
 */
private fun auxDirectory(symbolDirectory: String, auxDir: String = "auxil", block: () -> Unit) {
    val auxDirFile = File("$symbolDirectory/$auxDir")
    if (auxDirFile.exists().not()) {
        auxDirFile.mkdir()
    }
    block()
    auxDirFile.deleteRecursively()
}

/**
 * @author Hannah Schellekens
 */
private enum class Theme(val colourName: String) {

    LIGHT("lightforeground"),
    DARK("darkforeground");

    override fun toString() = colourName
}

package nl.hannahsten.texifyidea.util.files.impl

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.util.files.ReferencedFileSetCache
import nl.hannahsten.texifyidea.util.files.ReferencedFileSetService

/**
 * @author Hannah Schellekens
 */
class ReferencedFileSetServiceImpl : ReferencedFileSetService {

    private val cache = ReferencedFileSetCache()

    override fun referencedFileSetOf(psiFile: PsiFile) = cache.fileSetFor(psiFile)

    override fun rootFilesOf(psiFile: PsiFile): Set<PsiFile> = cache.rootFilesFor(psiFile)

    override fun dropCaches(file: VirtualFile) = cache.dropCaches(file)

    override fun dropAllCaches() = cache.dropAllCaches()
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages

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.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.DefaultEnvironment
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.DEFAULT
import nl.hannahsten.texifyidea.lang.commands.LatexCommand
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.PackageUtils
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.definitionsAndRedefinitionsInFileSet
import nl.hannahsten.texifyidea.util.findCommandDefinitions
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.magic.PackageMagic
import nl.hannahsten.texifyidea.util.parser.*
import java.util.*

/**
 * Currently works for built-in commands and environments.
 *
 * @author Hannah Schellekens
 */
open class LatexMissingImportInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "MissingImport"

    override val ignoredSuppressionScopes = EnumSet.of(MagicCommentScope.GROUP)!!

    override fun getDisplayName() = "Missing imports"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        if (!TexifySettings.getInstance().automaticDependencyCheck) {
            return emptyList()
        }

        val descriptors = descriptorList()

        val includedPackages = file.includedPackages()
        analyseCommands(file, includedPackages, descriptors, manager, isOntheFly)
        analyseEnvironments(file, includedPackages, descriptors, manager, isOntheFly)

        return descriptors
    }

    private fun analyseEnvironments(
        file: PsiFile, includedPackages: Collection<LatexPackage>,
        descriptors: MutableList<ProblemDescriptor>, manager: InspectionManager,
        isOntheFly: Boolean
    ) {
        val environments = file.childrenOfType(LatexEnvironment::class)
        val defined = file.definitionsAndRedefinitionsInFileSet().asSequence()
            .filter { it.isEnvironmentDefinition() }
            .mapNotNull { it.requiredParameter(0) }
            .toSet()

        outerLoop@ for (env in environments) {
            // Don't consider environments that have been defined.
            if (env.name()?.text in defined) {
                continue
            }

            val name = env.name()?.text ?: continue
            val environment = DefaultEnvironment[name] ?: continue
            val pack = environment.dependency

            if (pack == DEFAULT || includedPackages.contains(pack)) {
                continue
            }

            // Packages included in other packages
            for (packageInclusion in PackageMagic.packagesLoadingOtherPackages) {
                if (packageInclusion == pack && includedPackages.contains(packageInclusion.key)) {
                    continue@outerLoop
                }
            }

            descriptors.add(
                manager.createProblemDescriptor(
                    env,
                    TextRange(7, 7 + name.length),
                    "Environment requires ${pack.name} package",
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly,
                    ImportEnvironmentFix(pack.name)
                )
            )
        }
    }

    private fun analyseCommands(
        file: PsiFile, includedPackages: Collection<LatexPackage>,
        descriptors: MutableList<ProblemDescriptor>, manager: InspectionManager,
        isOntheFly: Boolean
    ) {
        // This loops over all commands, so we don't want to do this again for every command in the file for performance
        val commandDefinitionsInProject = file.project.findCommandDefinitions().map { it.definedCommandName() }

        val commands = file.commandsInFile()
        commandLoop@ for (command in commands) {
            // If we are actually defining the command, then it doesn't need any dependency
            if (command.parent.firstParentOfType(LatexCommands::class).isCommandDefinition()) {
                continue
            }

            // If defined within the project, also fine
            if (commandDefinitionsInProject.contains(command.name)) {
                continue
            }

            val name = command.commandToken.text.substring(1)
            val latexCommands = LatexCommand.lookup(name) ?: continue

            // In case there are multiple commands with this name, we don't know which one the user wants.
            // So we don't know which of the dependencies the user needs: we assume that if at least one of them is present it will be the right one.
            val dependencies = latexCommands.map { it.dependency }.toSet()

            if (dependencies.isEmpty() || dependencies.any { it.isDefault }) {
                continue
            }

            // Packages included in other packages
            for (packageInclusion in PackageMagic.packagesLoadingOtherPackages) {
                if (packageInclusion.value.intersect(dependencies).isNotEmpty() && includedPackages.contains(packageInclusion.key)) {
                    continue@commandLoop
                }
            }

            // If none of the dependencies are included
            if (includedPackages.toSet().intersect(dependencies).isEmpty()) {
                // We know dependencies is not empty
                val range = TextRange(0, latexCommands.minByOrNull { it.command.length }!!.command.length + 1)
                val dependencyNames = dependencies.joinToString { it.name }.replaceAfterLast(", ", "or ${dependencies.last().name}")
                val fixes = dependencies.map { ImportCommandFix(it) }.toTypedArray()
                descriptors.add(
                    manager.createProblemDescriptor(
                        command,
                        range,
                        "Command requires $dependencyNames package",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        *fixes
                    )
                )
            }
        }
    }

    /**
     * @author Hannah Schellekens
     */
    private class ImportCommandFix(val pack: LatexPackage) : LocalQuickFix {

        override fun getFamilyName() = "Add import for package '${pack.name}' which provides this command"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val file = command.containingFile

            if (!PackageUtils.insertUsepackage(file, pack)) {
                Notification("LaTeX", "Conflicting package detected", "The package ${pack.name} was not inserted because a conflicting package was detected.", NotificationType.INFORMATION).notify(project)
            }
        }
    }

    /**
     * @author Hannah Schellekens
     */
    private class ImportEnvironmentFix(val import: String) : LocalQuickFix {

        override fun getFamilyName() = "Add import for package '$import' which provides this environment"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val environment = descriptor.psiElement as? LatexEnvironment ?: return
            val thingy = DefaultEnvironment.fromPsi(environment) ?: return
            val file = environment.containingFile

            PackageUtils.insertUsepackage(file, thingy.dependency)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.PackageUtils
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.magic.GeneralMagic
import nl.hannahsten.texifyidea.util.projectSearchScope
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import java.util.*

class LatexPackageCouldNotBeFound : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup = InsightGroup.LATEX

    override val inspectionId: String =
        "PackageCouldNotBeFound"

    override fun getDisplayName(): String {
        return "Package could not be found locally or on CTAN"
    }

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()
        val ctanPackages = PackageUtils.CTAN_PACKAGE_NAMES.map { it.lowercase(Locale.getDefault()) }
        val customPackages = LatexDefinitionIndex.getCommandsByName("\\ProvidesPackage", file.project, file.project.projectSearchScope)
            .map { it.requiredParameter(0) }
            .map { it?.lowercase(Locale.getDefault()) }
        val packages = ctanPackages + customPackages

        val commands = file.childrenOfType(LatexCommands::class)
            .filter { it.name == "\\usepackage" || it.name == "\\RequirePackage" }

        for (command in commands) {
            val `package` = command.getRequiredParameters().firstOrNull()?.lowercase(Locale.getDefault())
            if (!packages.contains(`package`)) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        command,
                        displayName,
                        GeneralMagic.noQuickFix,
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages

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.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.parser.childrenOfType

class LatexPackageNameDoesNotMatchFileNameInspection : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup = InsightGroup.LATEX

    override val inspectionId: String =
        "PackageNameDoesNotMatchFileName"

    override fun getDisplayName(): String {
        return "Package name does not match file name"
    }

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()

        val commands = file.childrenOfType(LatexCommands::class)
            .filter { it.name == "\\ProvidesPackage" }

        for (command in commands) {
            val providesName = command.getRequiredParameters().firstOrNull()?.split("/")?.last()
            val fileName = file.name.removeSuffix(".sty")
            if (fileName != providesName) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        command,
                        displayName,
                        PackageNameMatchFileNameQuickFix,
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }
        }

        return descriptors
    }

    object PackageNameMatchFileNameQuickFix : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Fix package name"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val providesCommand = descriptor.psiElement as LatexCommands
            val newCommandText = providesCommand.let {
                it.text.replace(
                    it.getRequiredParameters().first().split("/").last() + "}",
                    it.containingFile.name.removeSuffix(".sty") + "}"
                )
            }
            val newCommand = LatexPsiHelper(project).createFromText(newCommandText).firstChild

            val parent = providesCommand.parent
            parent.node.replaceChild(providesCommand.node, newCommand.node)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
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
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.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
import nl.hannahsten.texifyidea.util.TexLivePackages
import nl.hannahsten.texifyidea.util.projectSearchScope
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.requiredParameter
import nl.hannahsten.texifyidea.util.runCommand
import java.util.*

/**
 * Check if a LaTeX package is not installed (only for TeX Live, since MiKTeX downloads them automatically).
 */
class LatexPackageNotInstalledInspection : TexifyInspectionBase() {

    // This caches packages which are not installed, which is needed
    // otherwise we are running the expensive call to tlmgr basically on
    // every letter typed - exactly the same call with the same results
    private val knownNotInstalledPackages = mutableSetOf<String>()

    override val inspectionGroup: InsightGroup = InsightGroup.LATEX

    override val inspectionId: String =
        "PackageNotInstalled"

    override fun getDisplayName(): String {
        return "Package is not installed"
    }

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        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.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) {
                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<InputFileReference>().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`)
                    }
                }
            }
        }
        return descriptors
    }

    private class InstallPackage(val filePointer: SmartPsiElementPointer<PsiFile>, val packageName: String, val knownNotInstalledPackages: MutableSet<String>) : LocalQuickFix {

        override fun getFamilyName(): String = "Install $packageName"

        /**
         * Install the package in the background and add it to the list of installed
         * packages when done.
         */
        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            // I don't know if you actually could install multiple packages
            // with one fix, but it's not a bad idea to clear cache once in a while
            knownNotInstalledPackages.clear()
            val tlmgrExecutable = LatexSdkUtil.getExecutableName("tlmgr", project)

            ProgressManager.getInstance()
                .run(object : Task.Backgroundable(project, "Installing $packageName...") {
                    override fun run(indicator: ProgressIndicator) {
                        val tlname = TexLivePackages.findTexLiveName(this, packageName, project)

                        if (tlname == null) {
                            Notification(
                                "LaTeX",
                                "Package $packageName not installed",
                                "Package $packageName was not installed because tlmgr could not find $packageName.sty anywhere. Try to install the package manually.",
                                NotificationType.ERROR
                            ).notify(project)

                            indicator.cancel()
                        }

                        title = "Installing $packageName..."
                        val output = "$tlmgrExecutable install $tlname".runCommand()
                        if (output?.contains("tlmgr update --self") == true) {
                            title = "Updating tlmgr..."
                            "$tlmgrExecutable update --self".runCommand()
                            title = "Installing $packageName..."
                            "$tlmgrExecutable install $tlname".runCommand()
                        }
                    }

                    override fun onSuccess() {
                        TexLivePackages.packageList.add(packageName)
                        // Rerun inspections
                        DaemonCodeAnalyzer.getInstance(project)
                            .restart(
                                filePointer.containingFile
                                    ?: return
                            )
                    }
                })
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages

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.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.files.findRootFile
import java.io.File
import kotlin.math.max

class LatexPackageSubdirectoryInspection : TexifyInspectionBase() {

    override val inspectionGroup: InsightGroup = InsightGroup.LATEX

    override val inspectionId: String = "PackageSubdirectoryInspection"

    override fun getShortName(): String {
        return "LatexPackageSubdirectory"
    }

    override fun getDisplayName(): String =
        "Package name does not have the correct directory"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()
        val commands = file.childrenOfType(LatexCommands::class)
            .filter { it.name == "\\ProvidesPackage" }

        for (command in commands) {
            val parameter = command.getRequiredParameters().firstOrNull() ?: continue
            val lastSlashIndex = parameter.indexOfLast { it == '/' }
            val providedDir = parameter.removeRange(max(0, lastSlashIndex), parameter.length)
            val rootDir = file.findRootFile().containingDirectory ?: continue
            val dir = file.containingDirectory ?: continue
            val subDir = dir.toString().removePrefix(rootDir.toString()).removePrefix(File.separator).replace(File.separatorChar, '/')
            if (subDir != providedDir) {
                descriptors.add(
                    manager.createProblemDescriptor(
                        command,
                        displayName,
                        FixSubdirectoryQuickFix(providedDir, if (providedDir.isEmpty()) "$subDir/" else subDir),
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly
                    )
                )
            }
        }
        return descriptors
    }

    inner class FixSubdirectoryQuickFix(private val oldDir: String, private val newDir: String) : LocalQuickFix {

        override fun getFamilyName(): String =
            "Change LaTeX command to match directory structure"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val providesCommand = descriptor.psiElement as LatexCommands
            val newCommandText = providesCommand.text.replace(
                "{$oldDir", "{$newDir"
            )
            val newCommand = LatexPsiHelper(project).createFromText(newCommandText).firstChild

            val parent = providesCommand.parent
            parent.node.replaceChild(providesCommand.node, newCommand.node)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.probablebugs.packages

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 com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import nl.hannahsten.texifyidea.index.file.LatexExternalCommandIndex
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexRegularCommand
import nl.hannahsten.texifyidea.util.parser.definedCommandName
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.definitionsInFileSet
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.insertUsepackage
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.cmd

/**
 * Warn when the user uses a command that is not defined in any included packages or LaTeX base.
 * This is an extension of [LatexMissingImportInspection], however, because this also
 * complains about commands that are not hardcoded in TeXiFy but come from any package,
 * and this index of commands is far from complete, it has to be disabled by default,
 * and thus cannot be included in the mentioned inspection.
 *
 * @author Thomas
 */
class LatexUndefinedCommandInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "UndefinedCommand"

    override fun getDisplayName() = "Command is not defined"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val includedPackages = file.includedPackages().toSet().plus(LatexPackage.DEFAULT)
        val commandsInFile = file.commandsInFile()
        val commandNamesInFile = commandsInFile.map { it.name }
        // The number of indexed commands can be quite large (50k+) so we filter the large set based on the small one (in this file).
        val indexedCommands = FileBasedIndex.getInstance().getAllKeys(LatexExternalCommandIndex.id, file.project)
            .filter { it in commandNamesInFile }
            .associateWith { command ->
                val containingPackages = FileBasedIndex.getInstance().getContainingFiles(LatexExternalCommandIndex.id, command, GlobalSearchScope.everythingScope(file.project))
                    .map { LatexPackage.create(it) }
                    .toSet()
                containingPackages
            }
        val magicCommands = LatexRegularCommand.ALL.associate { Pair(it.cmd, setOf(it.dependency)) }
        val userDefinedCommands = file.definitionsInFileSet().filter { it.name in CommandMagic.commandDefinitions }
            .map { it.definedCommandName() }
            .associateWith { setOf(LatexPackage.DEFAULT) }

        // Join all the maps, map command name (with backslash) to all packages it is defined in
        val allKnownCommands = (indexedCommands.keys + magicCommands.keys + userDefinedCommands.keys).associateWith {
            indexedCommands.getOrDefault(it, setOf()) + magicCommands.getOrDefault(it, setOf()) + userDefinedCommands.getOrDefault(it, setOf())
        }

        return commandsInFile.filter { allKnownCommands.getOrDefault(it.name, emptyList()).intersect(includedPackages).isEmpty() }
            .map { command ->
                manager.createProblemDescriptor(
                    command,
                    "Command ${command.name} is not defined",
                    allKnownCommands.getOrDefault(command.name, emptyList()).map { ImportPackageFix(it) }.toTypedArray(),
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly,
                    false
                )
            }
    }

    private class ImportPackageFix(val dependency: LatexPackage) : LocalQuickFix {

        override fun getFamilyName() = "Add import for package ${dependency.name}"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            descriptor.psiElement.containingFile.insertUsepackage(dependency)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting.spacing

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.DefaultEnvironment
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSMATH
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.inDirectEnvironment
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.parser.isComment

/**
 * @author Sten Wessel
 */
open class LatexEllipsisInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override fun getDisplayName() = "Ellipsis with ... instead of \\ldots or \\dots"

    override val inspectionId = "Ellipsis"

    private fun shouldIgnore(elementAtCaret: PsiElement?): Boolean {
        if (elementAtCaret == null) return false
        return elementAtCaret.isComment() || elementAtCaret.inDirectEnvironment(DefaultEnvironment.TIKZPICTURE.environmentName)
    }

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()
        val texts = file.childrenOfType(LatexNormalText::class)

        for (text in texts) {
            ProgressManager.checkCanceled()

            for (match in PatternMagic.ellipsis.findAll(text.text)) {
                if (shouldIgnore(file.findElementAt(match.range.first + text.startOffset))) {
                    continue
                }

                descriptors.add(
                    manager.createProblemDescriptor(
                        text,
                        match.range.toTextRange(),
                        "Ellipsis with ... instead of command",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        InsertEllipsisCommandFix(text.inMathContext())
                    )
                )
            }
        }

        return descriptors
    }

    /**
     * @author Sten Wessel
     */
    private class InsertEllipsisCommandFix(val inMathMode: Boolean) : LocalQuickFix {

        override fun getFamilyName() = "Convert to ${if (inMathMode) "\\dots (amsmath package)" else "\\ldots"}"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val element = descriptor.psiElement
            val file = element.containingFile
            val document = PsiDocumentManager.getInstance(project).getDocument(file) ?: return
            val range = descriptor.textRangeInElement.shiftRight(element.textOffset)

            document.replaceString(range.startOffset, range.endOffset, if (inMathMode) "\\dots" else "\\ldots")

            if (inMathMode && AMSMATH !in file.includedPackages()) {
                file.insertUsepackage(AMSMATH)
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting.spacing

import com.intellij.codeInspection.ProblemDescriptor
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.lang.LatexPackage.Companion.AMSSYMB
import nl.hannahsten.texifyidea.util.insertUsepackage
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexExtremeInequalityInspection : TexifyRegexInspection(
    inspectionDisplayName = "Use the matching amssymb symbol for inequalities",
    inspectionId = "ExtremeInequality",
    errorMessage = { "Use the amssymb symbol instead." },
    pattern = Pattern.compile("(<\\s*<(?!\\s*<))|(<\\s*<\\s*<)|(>\\s*>(?!\\s*>))|(>\\s*>\\s*>)"),
    mathMode = true,
    replacement = { it, _ -> replacement(it) },
    replacementRange = this::replaceRange,
    quickFixName = { "Insert amssymb symbol." }
) {

    companion object {

        /**
         * Determines what the replacement for the quick fix must be.
         */
        fun replacement(it: Matcher) = if (it.group(1) != null) {
            "\\ll"
        }
        else if (it.group(2) != null) {
            "\\lll"
        }
        else if (it.group(3) != null) {
            "\\gg"
        }
        else "\\ggg"

        /**
         * Determines what range must be replaced.
         */
        fun replaceRange(it: Matcher): IntRange {
            for (i in 1..4) {
                if (it.group(i) != null) {
                    return it.groupRange(i)
                }
            }

            return IntRange.EMPTY
        }
    }

    override fun applyFixes(descriptor: ProblemDescriptor, replacementRanges: List<IntRange>, replacements: List<String>, groups: List<List<String>>) {
        super.applyFixes(descriptor, replacementRanges, replacements, groups)

        // We overrided applyFixes instead of applyFix because all fixes need to be applied together, and only after that we insert any required package.
        val file = descriptor.psiElement.containingFile ?: return
        file.insertUsepackage(AMSSYMB)
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting.spacing

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiRecursiveElementVisitor
import com.intellij.psi.util.PsiTreeUtil
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexMathContent
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import org.jetbrains.annotations.Nls

/**
 * Detects non-escaped common math functions like *sin*, *cos* and replaces them
 * with `\sin`, `\cos`.
 *
 * @author Sten Wessel
 */
class LatexMathOperatorEscapeInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    @Nls
    override fun getDisplayName() = "Non-escaped common math operators"

    override val inspectionId = "MathOperatorEscape"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()
        val pattern = PlatformPatterns.psiElement(LatexTypes.NORMAL_TEXT_WORD)
        val envs = PsiTreeUtil.findChildrenOfType(file, LatexMathContent::class.java)

        for (env in envs) {
            env.acceptChildren(object : PsiRecursiveElementVisitor() {
                override fun visitElement(element: PsiElement) {
                    ProgressManager.checkCanceled()
                    if (pattern.accepts(element)) {
                        fun hasMathParentBeforeTextParent() = PsiTreeUtil.collectParents(
                            element,
                            LatexMathContent::class.java,
                            false
                        ) { it is LatexCommands && it.name == "\\text" }.size > 0

                        fun descriptorAlreadyExists() = descriptors.firstOrNull {
                            it.psiElement == element && it.descriptionTemplate == "Non-escaped math operator"
                        } != null

                        if (element.text in SLASHLESS_MATH_OPERATORS && !descriptorAlreadyExists() && hasMathParentBeforeTextParent()) {
                            descriptors.add(
                                manager.createProblemDescriptor(
                                    element,
                                    "Non-escaped math operator",
                                    EscapeMathOperatorFix(),
                                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                                    isOntheFly
                                )
                            )
                        }
                    }
                    else super.visitElement(element)
                }
            })
        }
        return descriptors
    }

    /**
     * @author Sten Wessel
     */
    private class EscapeMathOperatorFix : LocalQuickFix {

        @Nls
        override fun getFamilyName(): String = "Escape math operator"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val element = descriptor.psiElement
            val document = PsiDocumentManager.getInstance(project).getDocument(element.containingFile)
            document?.insertString(element.textOffset, "\\")
        }
    }

    companion object {

        private val SLASHLESS_MATH_OPERATORS = CommandMagic.slashlessMathOperators.asSequence()
            .map { it.command }
            .toSet()
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting.spacing

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 com.intellij.psi.PsiWhiteSpace
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.LatexPackage
import nl.hannahsten.texifyidea.lang.commands.LatexGenericRegularCommand.*
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexNoMathContent
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.magic.CommandMagic
import nl.hannahsten.texifyidea.util.magic.PatternMagic
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.parser.lastChildOfType
import nl.hannahsten.texifyidea.util.parser.parentOfType
import java.util.*

/**
 * For now, only not using it before `\ref` or `\cite` will be detected.
 *
 * @author Hannah Schellekens
 */
open class LatexNonBreakingSpaceInspection : TexifyInspectionBase() {

    companion object {

        /**
         * All commands that should not have a forced breaking space.
         */
        val IGNORED_COMMANDS = setOf(
            "\\citet",
            "\\citet*",
            "\\Citet",
            "\\Citet*",
            "\\cref",
            "\\Cref",
            "\\cpageref",
            "\\autoref",
            "\\citeauthor",
            "\\textcite",
            "\\Textcite"
        )

        /**
         * Commands redefined by cleveref, such that no non-breaking space is needed anymore.
         */
        val CLEVEREF_REDEFINITIONS = setOf(THREF, VREF, VREFRANGE, FULLREF).map { it.commandWithSlash }
    }

    override val inspectionGroup = InsightGroup.LATEX

    override val inspectionId = "NonBreakingSpace"

    override val outerSuppressionScopes = EnumSet.of(MagicCommentScope.COMMAND)!!

    override fun getDisplayName() = "Non-breaking spaces before references"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
        val descriptors = descriptorList()
        val isCleverefLoaded = file.includedPackages().contains(LatexPackage.CLEVEREF)

        val commands = file.commandsInFile()
        for (command in commands) {
            // Only target references.
            if (!CommandMagic.reference.contains(command.name)) continue

            // Don't consider certain commands.
            if (command.name in IGNORED_COMMANDS) continue

            // Don't out-clever cleveref
            if (isCleverefLoaded && command.name in CLEVEREF_REDEFINITIONS) continue

            // Get the NORMAL_TEXT in front of the command.
            val sibling = command.prevSibling
                ?: command.parent?.prevSibling
                ?: command.parentOfType(LatexNoMathContent::class)?.prevSibling
                ?: continue

            val previousSentence = sibling.prevSibling
                ?: sibling.parent?.prevSibling
                ?: sibling.parentOfType(LatexNoMathContent::class)?.prevSibling
                ?: continue

            // When sibling is whitespace, it's obviously bad news. Must not have a newline
            if (sibling is PsiWhiteSpace) {
                val lastBitOfText = previousSentence.lastChildOfType(LatexNormalText::class) ?: previousSentence.lastChildOfType(LatexParameterText::class) ?: continue
                if (!PatternMagic.sentenceSeparatorAtLineEnd.matcher(file.text.subSequence(lastBitOfText.startOffset, lastBitOfText.endOffset)).find()) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            sibling,
                            "Reference without a non-breaking space",
                            WhitespaceReplacementFix(),
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                        )
                    )
                }
                continue
            }
        }

        return descriptors
    }

    /**
     * Replaces a complete [PsiWhiteSpace] element with `~`.
     *
     * @author Hannah Schellekens
     */
    private class WhitespaceReplacementFix : LocalQuickFix {

        override fun getFamilyName() = "Insert non-breaking space"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val whitespace = descriptor.psiElement as PsiWhiteSpace
            val file = whitespace.containingFile
            val document = file.document() ?: return
            val offset = whitespace.textOffset
            var replacement = "~"

            // First check if there already is a tilde in the normal text before.
            val texts = whitespace.prevSibling.childrenOfType(LatexNormalText::class)
            if (!texts.isEmpty()) {
                val text = texts.reversed().iterator().next()

                // When there is a tilde, destroy the whitespace.
                if (PatternMagic.endsWithNonBreakingSpace.matcher(text.text).find()) {
                    replacement = ""
                }
            }

            // Otherwise, just replace all the whitespace by a tilde.
            document.replaceString(offset, offset + whitespace.textLength, replacement)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting.spacing

import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.parser.inDirectEnvironment
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexSentenceEndWithCapitalInspection : TexifyRegexInspection(
    inspectionDisplayName = "End-of-sentence space after sentences ending with capitals",
    inspectionId = "SentenceEndWithCapital",
    errorMessage = { "Sentences ending with a capital letter should end with an end-of-sentence space" },
    pattern = Pattern.compile("[A-ZÀ-Ý](\\.)[ \\t]*\\n"),
    replacement = { _, _ -> "\\@." },
    replacementRange = { it.start(1)..it.start(1) + 1 },
    quickFixName = { "Add an end-of-sentence space" },
    cancelIf = { matcher, file -> isInElement<LatexCommands>(matcher, file) }
) {

    override fun checkContext(matcher: Matcher, element: PsiElement): Boolean {
        if (element.inDirectEnvironment(setOf("verbatim", "verbatim*", "lstlisting"))) {
            return false
        }
        return super.checkContext(matcher, element)
    }
}

package nl.hannahsten.texifyidea.inspections.latex.typesetting.spacing

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import nl.hannahsten.texifyidea.inspections.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.parser.childrenOfType
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.parser.inMathContext
import nl.hannahsten.texifyidea.util.magic.PatternMagic

/**
 * @author Hannah Schellekens
 */
open class LatexSpaceAfterAbbreviationInspection : TexifyInspectionBase() {

    override val inspectionGroup = InsightGroup.LATEX

    override fun getDisplayName() = "Normal space after abbreviation"

    override val inspectionId = "SpaceAfterAbbreviation"

    override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): MutableList<ProblemDescriptor> {
        val descriptors = descriptorList()

        val texts = file.childrenOfType(LatexNormalText::class)
        for (text in texts) {
            if (text.inMathContext()) {
                continue
            }

            // We find ranges where comments start and end, to check if matches are in there
            val commentMatcher = PatternMagic.comments.matcher(text.text)
            val commentParts = arrayListOf<IntRange>()
            while (commentMatcher.find()) {
                commentParts.add(commentMatcher.start()..commentMatcher.end())
            }

            val matcher = PatternMagic.abbreviationWithoutNormalSpace.matcher(text.text)
            while (matcher.find()) {
                val matchRange = matcher.start()..matcher.end()

                if (text.text.length < matcher.end() + 1) {
                    continue
                }

                // If the match is inside a comment, ignore it
                if (commentParts.any { it.first < matchRange.first && it.last > matchRange.last }) {
                    continue
                }

                descriptors.add(
                    manager.createProblemDescriptor(
                        text,
                        TextRange(matchRange.last - 2, matchRange.last),
                        "Abbreviation should be followed by a normal space",
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                        isOntheFly,
                        NormalSpaceFix(matchRange)
                    )
                )
            }
        }

        return descriptors
    }

    private fun isFollowedByWhitespace(text: LatexNormalText, matchRange: IntRange): Boolean {
        // Whitespace followed in the Normal Text.
        val string = text.text
        if (text.text.length > matchRange.last) {
            val spaceMaybe = string.substring(matchRange.last, matchRange.last + 1)
            if (matchRange.last < string.length && spaceMaybe.matches(Regex("\\s+"))) {
                return true
            }
        }

        // Whitespace as PsiWhitespace
        val content = text.parent?.parent ?: return false
        return content.nextSibling is PsiWhiteSpace
    }

    /**
     * @author Hannah Schellekens
     */
    open class NormalSpaceFix(private val whitespaceRange: IntRange) : LocalQuickFix {

        override fun getFamilyName() = "Insert normal space after abbreviation"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val element = descriptor.psiElement as LatexNormalText
            val file = element.containingFile
            val document = file.document() ?: return

            replaceNormalText(document, element)
        }

        private fun replaceNormalText(document: Document, normalText: LatexNormalText) {
            val start = normalText.textOffset + whitespaceRange.last - 1
            val end = normalText.textOffset + whitespaceRange.last
            document.replaceString(start, end, "\\ ")
        }
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.ui

import com.intellij.icons.AllIcons
import com.intellij.ide.IdeBundle
import com.intellij.ide.errorTreeView.ErrorTreeElement
import com.intellij.ide.errorTreeView.NewErrorTreeViewPanel
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.util.containsAny
import nl.hannahsten.texifyidea.util.remove
import java.util.*

class LatexCompileMessageTreeView(
    val project: Project,
    private val latexMessageList: MutableList<LatexLogMessage>,
    private val bibtexMessageList: MutableList<BibtexLogMessage>
) :
    NewErrorTreeViewPanel(project, null) {

    fun config(): LatexErrorTreeViewConfiguration = LatexErrorTreeViewConfiguration.getInstance(myProject)

    override fun fillRightToolbarGroup(group: DefaultActionGroup) {
        // Use myProject (from NewErrorTreeViewPanel) because somehow project is null
        LatexKeywordFilter.values().forEach { group.add(FilterKeywordAction(it, myProject)) }
        group.addAll(FilterBibtexAction(myProject), ExpandAllAction(), CollapseAllAction())

        // Search for the information action toggle so we can remove it (as we don't use it).
        val informationAction = group.childActionsOrStubs.find {
            it.templateText?.contains(IdeBundle.messagePointer("action.show.infos").get()) == true
        }
        if (informationAction != null) {
            group.remove(informationAction)
        }
        super.fillRightToolbarGroup(group)
    }

    /**
     * Get all elements that are currently in the tree.
     *
     * Elements are located as direct children of a file, which are direct children of the root.
     * Therefore, we only have to look one level deep to get all the messages.
     */
    private fun getAllElements(): List<ErrorTreeElement> {
        this@LatexCompileMessageTreeView.errorViewStructure.let { tree ->
            return tree.getChildElements(tree.rootElement).flatMap {
                tree.getChildElements(it).toList()
            }
        }
    }

    /**
     * Get selected elements.
     */
    private fun getSelectedElements(): List<ErrorTreeElement> {
        val selectedRows = myTree.selectionRows ?: return emptyList()
        val tree = errorViewStructure
        val rows = tree.getChildElements(tree.rootElement).flatMap {
            listOf(it) + tree.getChildElements(it).toList()
        }
        return rows.filterIndexed { index, _ -> index in selectedRows }
    }

    /**
     * Add this [message] to the tree view.
     */
    private fun addMessage(message: LatexLogMessage) {
        // Correct the index because the tree view starts counting at line 0 instead of line 1.
        addMessage(message.type.category, arrayOf(message.message), message.file, message.line - 1, -1, null)
    }

    private fun addMessage(logMessage: BibtexLogMessage) {
        addMessage(
            logMessage.type.category, arrayOf(logMessage.message), logMessage.file,
            // Treeview starts at 0
            (logMessage.line ?: 0) - 1, -1, null
        )
    }

    /**
     * Apply all filters to all messages in the [latexMessageList] list.
     */
    fun applyFiltersToAllMessages() {
        latexMessageList.forEach { applyFilters(it) }
        bibtexMessageList.forEach { applyFilters(it) }
    }

    /**
     * Apply all filters to [logMessage].
     */
    fun applyFilters(logMessage: LatexLogMessage) {
        val hide = logMessage.message.lowercase(Locale.getDefault()).containsAny(
            LatexKeywordFilter.values()
                .filter { f -> config().showKeywordWarnings[f]?.not() ?: false }
                .map { f -> f.triggers }.flatten().toSet()
        )
        if (!hide && logMessage !in this) {
            addMessage(logMessage)
        }
        if (hide && logMessage in this) {
            errorViewStructure.removeElement(
                // This element exists because we have checked for it with `logMessage in this`
                getAllElements().first { e -> e.fullString() == logMessage.toTreeViewString() }
            )
        }
        updateTree()
    }

    fun applyFilters(logMessage: BibtexLogMessage) {
        if (config().showBibtexWarnings && logMessage !in this) {
            addMessage(logMessage)
        }
        if (!config().showBibtexWarnings && logMessage in this) {
            errorViewStructure.removeElement(
                getAllElements().first { e -> e.fullString() == logMessage.toTreeViewString() }
            )
        }
    }

    // Remove commas, because they are inserted for line numbers > 999
    private fun ErrorTreeElement.fullString(): String = "${exportTextPrefix.remove(",")} ${text.joinToString()}"

    /**
     * Check if a message is currently in the tree.
     */
    private operator fun contains(message: LatexLogMessage): Boolean =
        getAllElements().map { it.fullString() }.contains(message.toTreeViewString())

    private operator fun contains(message: BibtexLogMessage): Boolean =
        getAllElements().map { it.fullString() }.contains(message.toTreeViewString())

    inner class FilterKeywordAction(private val keyword: LatexKeywordFilter, val project: Project) :
        ToggleAction("Show $keyword messages", "Show $keyword messages", keyword.icon), DumbAware {

        override fun isSelected(e: AnActionEvent): Boolean = config().showKeywordWarnings[keyword] ?: true

        override fun setSelected(e: AnActionEvent, state: Boolean) {
            config().showKeywordWarnings[keyword] = state
            applyFiltersToAllMessages()
        }

        override fun getActionUpdateThread() = ActionUpdateThread.EDT
    }

    inner class FilterBibtexAction(val project: Project) :
        ToggleAction("Show Bibtex Messages by Latexmk", "", TexifyIcons.DOT_BIB), DumbAware {

        override fun isSelected(e: AnActionEvent): Boolean = config().showBibtexWarnings

        override fun setSelected(e: AnActionEvent, state: Boolean) {
            config().showBibtexWarnings = state
            applyFiltersToAllMessages()
        }

        override fun getActionUpdateThread() = ActionUpdateThread.EDT
    }

    inner class ExpandAllAction : AnAction("Expand All", "", AllIcons.Actions.Expandall), DumbAware {

        override fun actionPerformed(e: AnActionEvent) {
            expandAll()
            config().expanded = true
        }
    }

    inner class CollapseAllAction : AnAction("Collapse All", "", AllIcons.Actions.Collapseall), DumbAware {

        override fun actionPerformed(e: AnActionEvent) {
            collapseAll()
            config().expanded = false
        }
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.ui

import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.XmlSerializerUtil

/**
 * Save the chosen treeview filters.
 * Note that [showBibtexWarnings] only applies to bibtex warnings shown for the latex run config, like for latexmk
 */
@State(
    name = "LatexErrorTreeViewConfiguration",
    storages = [
        (
            Storage(
                StoragePathMacros.WORKSPACE_FILE
            )
            )
    ]
)

data class LatexErrorTreeViewConfiguration(
    var showKeywordWarnings: MutableMap<LatexKeywordFilter, Boolean> = LatexKeywordFilter.values().associateWith { true }.toMutableMap(),
    var showBibtexWarnings: Boolean = true,
    var expanded: Boolean = true // Unfortunately we cannot use this, because expandAll() apparently only works in Actions
) :
    PersistentStateComponent<LatexErrorTreeViewConfiguration> {

    companion object {

        @JvmStatic
        fun getInstance(project: Project): LatexErrorTreeViewConfiguration =
            project.getService(LatexErrorTreeViewConfiguration::class.java)
    }

    override fun getState(): LatexErrorTreeViewConfiguration {
        return this
    }

    override fun loadState(state: LatexErrorTreeViewConfiguration) {
        XmlSerializerUtil.copyBean(state, this)
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.ui

import com.intellij.icons.AllIcons
import javax.swing.Icon

enum class LatexKeywordFilter(val triggers: List<String>, val icon: Icon) {

    OVERFULL_HBOX(listOf("overfull \\hbox", "underfull \\hbox", "overfull \\vbox", "underfull \\vbox"), icon = AllIcons.General.ArrowSplitCenterH);

    override fun toString(): String {
        return triggers.dropLast(1).joinToString() + " and " + triggers.last()
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle log messages which use the procedure aux_end_err in bibtex.web (line 3352).
 */
object AuxEndErrBibtexMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        BibtexLogMagicRegex.auxEndErr.find(window.lastOrNull() ?: "")?.apply {
            return BibtexLogMessage(groups["message"]?.value ?: window.last(), groups["file"]?.value, null, BibtexLogMessageType.ERROR)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle log messages which use the procedure aux_err_print in bibtex.web (line 2698).
 * The actual message will probably be directly before the ---line but may be on the same or previous line.
 */
object AuxErrPrintBibtexMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        BibtexLogMagicRegex.auxErrPrint.find(window.lastOrNull() ?: "")?.apply {
            if (window.size < 2) return null

            // Check if the message is on the same or previous line
            val message = if (this.range.first > 0) {
                window.last().substring(0, this.range.first)
            }
            else {
                window[window.size - 2]
            }
            return BibtexLogMessage(message, groups["file"]?.value, groups["line"]?.value?.toInt(), BibtexLogMessageType.ERROR)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle biber error messages with file and line information.
 */
object BiberErrorBibtexSubsystemMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        BibtexLogMagicRegex.biberErrorBibtexSubsystem.find(window.lastOrNull() ?: "")?.apply {
            // Though we have the name of the auxiliary file, we just assume it is the same as the opened bib file
            // Also note that the line is not exact (because in the auxiliary file) but hopefully close
            return BibtexLogMessage(groups["message"]?.value ?: "", currentFile, groups["line"]?.value?.toInt(), BibtexLogMessageType.ERROR)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle biber error messages which do not have any information about file or line.
 */
object BiberErrorMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        BibtexLogMagicRegex.biberError.find(window.lastOrNull() ?: "")?.apply {
            return BibtexLogMessage(groups["message"]?.value ?: "", currentFile, null, BibtexLogMessageType.ERROR)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle log messages which use the procedure bst_ex_warn_print on line 6874 of bibtex.web.
 * Even though it shows a line and file, this is from the bst file and most likely not the problem.
 */
object BstExWarnPrintBibtexMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        BibtexLogMagicRegex.bstExWarnPrint.find(window.lastOrNull() ?: "")?.apply {
            if (window.size < 2) return null
            return BibtexLogMessage(window[window.size - 2], currentFile, null, BibtexLogMessageType.ERROR)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle log messages which use the Clean up and leave macro in bibtex.web (line 11324)
 * and for which we assume the message is on the previous line.
 */
object CleanUpAndLeaveBibtexMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        BibtexLogMagicRegex.cleanUpAndLeave.find(window.lastOrNull() ?: "")?.apply {
            if (window.size < 2) return null
            return BibtexLogMessage(window[window.size - 2], groups["file"]?.value, groups["line"]?.value?.toInt(), BibtexLogMessageType.ERROR)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle log messages which use the procedure nonexistent_cross_reference_error on line 6617 of bibtex.web.
 * Note that it consists of two lines, without any indication that it does.
 */
object NonexistentCrossReferenceBibtexMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        BibtexLogMagicRegex.nonexistentCrossReferenceError.find(window.lastOrNull() ?: "")?.apply {
            if (window.size < 2) return null
            return BibtexLogMessage(window.takeLast(2).joinToString(" "), currentFile, null, BibtexLogMessageType.ERROR)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle log messages which use the procedure bib_ln_num_print on line 5045 in bibtex.web.
 * Note that warnings have '--' before 'line', and errors '---' (see bib_err_print vs bib_warn_print).
 */
object BibLnNumPrintBibtexMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        if (window.size < 3) return null
        // This case is handled by WarningBibtexMessageHandler
        if (BibtexLogMagicRegex.warning.matches(window[window.size - 2])) return null

        BibtexLogMagicRegex.bibLnNumPrint.find(window.lastOrNull() ?: "")?.apply {
            // Check if the message is on the same or previous line
            val message = if (this.range.first > 0) {
                window.last().substring(0, this.range.first)
            }
            else {
                window[window.size - 2]
            }
            return BibtexLogMessage(message, groups["file"]?.value, groups["line"]?.value?.toInt(), BibtexLogMessageType.WARNING)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle biber warning messages which do have information about the line number.
 */
object BiberWarningBibtexSubsystemMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        BibtexLogMagicRegex.biberWarningBibtexSubsystem.find(window.lastOrNull() ?: "")?.apply {
            return BibtexLogMessage(groups["message"]?.value ?: "", currentFile, groups["line"]?.value?.toInt(), BibtexLogMessageType.WARNING)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle biber warning messages which do not have any information about file or line.
 */
object BiberWarningMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        val regexes = listOf(BibtexLogMagicRegex.biberWarningInFile, BibtexLogMagicRegex.biberWarning)
        regexes.forEach { regex ->
            regex.find(window.lastOrNull() ?: "")?.apply {
                return BibtexLogMessage(groups["message"]?.value ?: "", currentFile, null, BibtexLogMessageType.WARNING)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle log messages which use line 3937 in bibtex.web, which is a special case where 'Warning--' and '--line' appear on the same line.
 */
object NoFieldsBibtexMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        // This case is handled by WarningBibtexMessageHandler
        BibtexLogMagicRegex.noFields.find(window.lastOrNull() ?: "")?.apply {
            return BibtexLogMessage(groups["message"]?.value ?: "", groups["file"]?.value, groups["line"]?.value?.toInt(), BibtexLogMessageType.WARNING)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.bibtex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessage
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMessageType
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexMessageHandler

/**
 * Handle log messages that start with 'Warning--', of which there are a couple of versions.
 * Since --line may still appear on the next line, we catch this message in the middle of the buffer.
 */
object WarningBibtexMessageHandler : BibtexMessageHandler() {

    override fun findMessage(window: List<String>, currentFile: String): BibtexLogMessage? {
        if (window.size < 3) return null
        // Index of the middle element of the buffer (might not be fully filled)
        // This does assume that there will be at least two more lines after the last relevant line (generally true, with Process finished message)
        val middleIndex = window.size - 3

        // Handled in NoFieldsBibtexMessageHandler
        if (BibtexLogMagicRegex.bibLnNumPrint.containsMatchIn(window[middleIndex])) return null

        BibtexLogMagicRegex.warning.find(window[middleIndex])?.apply {
            var file = currentFile
            var line: Int? = null
            // Match instead of find in order to avoid matching 'while executing--.+', as we assume in that case that the error is in the bib, not in the bst
            BibtexLogMagicRegex.bibLnNumPrint.matchEntire(window[middleIndex + 1])?.apply line@{
                line = groups["line"]?.value?.toInt()
                file = groups["file"]?.value ?: return@line
            }

            // Handle a message which spans two lines for some reason (line 6633 in bibtex.web)
            val message = if (BibtexLogMagicRegex.nestedCrossReference.matches(window[middleIndex])) {
                groups["message"]?.value + " " + window[middleIndex + 1]
            }
            else {
                groups["message"]?.value
            }

            return BibtexLogMessage(message ?: "", file, line, BibtexLogMessageType.WARNING)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.FILE_LINE_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LATEX_ERROR_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LINE_WIDTH
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.PDFTEX_ERROR_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.directLuaError
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

object LatexErrorHandler : LatexMessageHandler(
    LatexLogMessageType.ERROR,
    """$FILE_LINE_REGEX (?<message>.+)""".toRegex(),
    """^$LATEX_ERROR_REGEX (?<message>.+)""".toRegex(),
    """^$PDFTEX_ERROR_REGEX (?<message>.+)""".toRegex(),
    directLuaError,
    "(?<message>makeindex: .+)".toRegex(),
    "(?<message>Can't create output index file .+)".toRegex(),
) {

    private val messageProcessors = listOf(LatexPackageErrorProcessor, LatexRemoveErrorTextProcessor)

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.forEach {
            it.find(text)?.apply {
                val (line, fileName) = try {
                    Pair(groups["line"]?.value?.toInt(), groups["file"]?.value?.trim() ?: currentFile)
                }
                catch (e: IllegalArgumentException) {
                    Pair(null, currentFile)
                }

                var message = groups["message"]?.value?.trim() ?: ""

                // Since 'text' is two lines join together, check line length to find out whether the next line
                // is also part of the message (and remove it if not)
                if (text.removeSuffix(newText.trim()).length < LINE_WIDTH - 1) {
                    message = message.removeSuffix(newText.trim())
                }

                // Process a found error message (e.g. remove "LaTeX Error:")
                val processedMessage = messageProcessors.firstNotNullOfOrNull { p ->
                    if (p.regex.any { r -> r.containsMatchIn(message) }) p.postProcess(p.process(message)) else null
                } ?: message
                    .replace("<inserted text>", "")
                    .replace("<to be read again>", "")

                val trimmedMessage = processedMessage.replace("\\s+".toRegex(), " ")

                return LatexLogMessage(trimmedMessage, fileName, line ?: -1, messageType)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex

abstract class LatexErrorMessageProcessor(vararg val regex: Regex) {

    abstract fun process(message: String): String?
    fun postProcess(message: String?): String? = message
        ?.replace("See the LaTeX manual or LaTeX Companion for explanation.", "")
        ?.trim()
}

/**
 * LaTeX Error: text -> text
 */
object LatexRemoveErrorTextProcessor : LatexErrorMessageProcessor("""LaTeX Error:""".toRegex(), """pdfTeX error:""".toRegex()) {

    override fun process(message: String): String? {
        regex.forEach {
            if (it.containsMatchIn(message)) return it.replace(message, "").trim()
        }
        return null
    }
}

/**
 * Package amsmath error: text -> amsmath: text
 */
object LatexPackageErrorProcessor : LatexErrorMessageProcessor("""^Package ${LatexLogMagicRegex.PACKAGE_REGEX} Error:""".toRegex()) {

    override fun process(message: String): String? {
        regex.forEach {
            val `package` = it.find(message)?.groups?.get("package")?.value ?: return@forEach
            return "${`package`}: ${it.replace(message, "").trim()}"
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.fixMeError
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

/**
 * Errors by the fix me package.
 */
object LatexFixMeErrorMessageHandler : LatexMessageHandler(
    LatexLogMessageType.ERROR,
    fixMeError
) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.forEach {
            it.find(text)?.apply {
                val message = groups["message"]?.value ?: return null
                return LatexLogMessage("FiXme: $message", currentFile, groups["line"]?.value?.toInt() ?: -1, messageType)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.FILE_LINE_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

/**
 * Single-line errors, for which the l.<line number> appears in 'text' but should be excluded for the error message.
 */
object LatexSingleLineErrorMessageHandler : LatexMessageHandler(
    LatexLogMessageType.ERROR,
    """^$FILE_LINE_REGEX (?<message>.+)\s*l\.\d+""".toRegex()
) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.forEach {
            it.find(text)?.apply {
                val (line, fileName) = try {
                    Pair(groups["line"]?.value?.toInt(), groups["file"]?.value?.trim() ?: currentFile)
                }
                catch (e: IllegalArgumentException) {
                    Pair(null, currentFile)
                }

                val message = groups["message"]?.value ?: return null
                return LatexLogMessage(message, fileName, line ?: 1, messageType)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.errors

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.FILE_LINE_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LATEX_ERROR_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

object LatexUndefinedControlSequenceHandler : LatexMessageHandler(
    LatexLogMessageType.ERROR,
    // The last part (with line number and command) is optional because it may appear on the next line
    // Any line content may appear before the command which is undefined (which is the last thing on the line)
    """^$FILE_LINE_REGEX (?<message>Undefined control sequence.)(\s*l.\d+[\s\S]*(?<command>\\\w+)$)?""".toRegex(),
    """^$LATEX_ERROR_REGEX (?<message>Undefined control sequence\.)\s*l\.(?<line>\d+)\s*.*(?<command>\\\w+)${'$'}""".toRegex()
) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.forEach {
            it.find(text)?.apply {
                val line = try {
                    groups["line"]?.value?.toInt()
                }
                catch (ignored: IllegalArgumentException) {
                    null
                }
                val fileName = try {
                    groups["file"]?.value?.trim()
                }
                catch (ignored: IllegalArgumentException) {
                    currentFile
                }

                val message = "${groups["message"]?.value} ${groups["command"]?.value ?: ""}"

                return LatexLogMessage(message, fileName, line ?: 1, messageType)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageWithLineHandler

/**
 * Line number occurs in a unique location:
 *
 * (\end occurred inside a group at level 1)
 * ### simple group (level 1) entered at line 4 ({)
 */
object EndOccurredInsideGroupWarningHandler : LatexMessageWithLineHandler(
    LatexLogMessageType.WARNING,
    """^\(\\end occurred inside a group at level .+ entered at line (\d+) .+\)""".toRegex()
)

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageWithLineHandler

/**
 * Similar to [EndOccurredInsideGroupWarningHandler].
 */
object EndOccurredWhenConditionWasIncompleteWarningHandler : LatexMessageWithLineHandler(
    LatexLogMessageType.WARNING,
    """^\(\\end occurred when .+ on line (\d+) was incomplete\)""".toRegex()
)

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.fixMeWarning
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

/**
 * Warnings by the fix me package.
 */
object LatexFixMeWarningMessageHandler : LatexMessageHandler(
    LatexLogMessageType.WARNING,
    fixMeWarning
) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.forEach {
            it.find(text)?.apply {
                val message = groups["message"]?.value ?: return null
                return LatexLogMessage("FiXme: $message", currentFile, groups["line"]?.value?.toInt() ?: -1, messageType)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType.WARNING
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

/**
 * Warnings of the form LaTeX Warning: ... on input line l
 */
object LatexLineWarningHandler : LatexMessageHandler(
    WARNING,
    """${LatexLogMagicRegex.LATEX_WARNING_REGEX}(?<message>.+)${LatexLogMagicRegex.LINE_REGEX}""".toRegex()
) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        LatexLineWarningHandler.regex.forEach {
            it.find(text)?.apply {
                val message = groups["message"]?.value?.trim() ?: return@apply
                val line = groups["line"]?.value?.toInt() ?: return@apply
                return LatexLogMessage(message.replace("(Font)             ", ","), fileName = currentFile, type = super.messageType, line = line)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LINE_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.PACKAGE_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType.WARNING
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

object LatexPackageWarningHandler : LatexMessageHandler(
    WARNING,
    """^Package $PACKAGE_REGEX Warning: (?<message>.+) $LINE_REGEX$""".toRegex(),
    """^Package $PACKAGE_REGEX Warning: (?<message>.+)$""".toRegex()
) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.forEach {
            it.find(text)?.apply {
                val `package` = groups["package"]?.value
                val unProcessedMessage = groups["message"]?.value ?: return@apply
                // Remove the (package) occurrences in the rest of the warning message.
                val message =
                    """\(${`package`}\)\s+""".toRegex()
                        .replace(unProcessedMessage, " ").trim()
                val line = try {
                    groups["line"]?.value?.toInt() ?: -1
                }
                // This is the way to check for presence of regex groups apparently
                catch (ignored: IllegalArgumentException) {
                    -1
                }
                return LatexLogMessage("${`package`}: $message", fileName = currentFile, type = super.messageType, line = line)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.pdfTeXWarning
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

/**
 * pdfTeX warning messages (do not necessarily start on a new line)
 */
object LatexPdftexWarningMessageHandler : LatexMessageHandler(
    LatexLogMessageType.WARNING,
    pdfTeXWarning
) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.forEach {
            it.find(text)?.apply {
                val message = groups["message"]?.value ?: return null
                return LatexLogMessage(message, currentFile, -1, messageType)
            }
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LATEX_WARNING_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.LINE_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex.REFERENCE_REGEX
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler

/**
 * Reference `<key>' on page <number> undefined (p927 LaTeX Companion).
 */
object LatexReferenceCitationWarningHandler : LatexMessageHandler(
    LatexLogMessageType.WARNING,
    """^$LATEX_WARNING_REGEX (?<ref>Reference|Citation) $REFERENCE_REGEX on page \d+ undefined $LINE_REGEX$""".toRegex()
) {

    override fun findMessage(text: String, newText: String, currentFile: String?): LatexLogMessage? {
        regex.first().find(text)?.apply {
            val ref = groups["ref"]?.value
            val label = groups["label"]?.value
            val message = "$ref $label undefined"
            val line = groups["line"]?.value?.toInt()
            return LatexLogMessage(message, currentFile, line ?: 1, messageType)
        }
        return null
    }
}

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageWithLineHandler

/**
 * Overfull hbox-like warnings, which have the line number at the end of the warning message in a special way specific to these TeX warnings only.
 */
object OverfullHboxWarningHandler : LatexMessageWithLineHandler(
    LatexLogMessageType.WARNING,
    """^(?:Loose \\hbox|Loose \\vbox|Overfull \\hbox|Overfull \\vbox|Tight \\hbox|Tight \\vbox|Underfull \\hbox|Underfull \\vbox) \(.+\) (?:detected at line (\d+)|has occurred while \\output is active|in alignment at lines (\d+)--\d+|in paragraph at lines (\d+)--\d+)""".toRegex()
)

package nl.hannahsten.texifyidea.run.latex.logtab.messagehandlers.warnings

import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageWithLineHandler

/**
 * Multi-line warning with line number.

LaTeX Warning: You have requested, on input line 5, version
`9999/99/99' of package test998,
but only version
`2020/04/08'
is available.

 */
object YouHaveRequestedOnInputLineVersionOfPackageWarningHandler : LatexMessageWithLineHandler(
    LatexLogMessageType.WARNING,
    """${LatexLogMagicRegex.LATEX_WARNING_REGEX} You have requested, on input line (\d+), .+""".toRegex()
)