#### Type of JetBrains IDE (IntelliJ, PyCharm, etc.) and version
IntelliJ 2020.2

#### Operating System
macOS

#### TeXiFy IDEA version
Latest alpha

#### Description
The following bit of text incorrectly triggers the `Sentence does not start on a new line` Inspection.

#### Minimal example to reproduce the problem

```latex
This is an abbreviation (ABC). % commemt
More text here.
```

diff --git a/src/nl/hannahsten/texifyidea/inspections/latex/LatexLineBreakInspection.kt b/src/nl/hannahsten/texifyidea/inspections/latex/LatexLineBreakInspection.kt
index 4e6d4f529..f33211117 100644
--- a/src/nl/hannahsten/texifyidea/inspections/latex/LatexLineBreakInspection.kt
+++ b/src/nl/hannahsten/texifyidea/inspections/latex/LatexLineBreakInspection.kt
@@ -47,8 +47,9 @@ open class LatexLineBreakInspection : TexifyInspectionBase() {
                 val startOffset = matcher.start()
                 val endOffset = matcher.end() + (endLine - offset)

-                // Do not trigger the inspection when in a comment.
-                if (file.findElementAt(startOffset + text.startOffset)?.isComment() == true) {
+                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
                 }

diff --git a/test/nl/hannahsten/texifyidea/inspections/latex/LatexLineBreakInspectionTest.kt b/test/nl/hannahsten/texifyidea/inspections/latex/LatexLineBreakInspectionTest.kt
index d1ab3f4b5..db6ce91bc 100644
--- a/test/nl/hannahsten/texifyidea/inspections/latex/LatexLineBreakInspectionTest.kt
+++ b/test/nl/hannahsten/texifyidea/inspections/latex/LatexLineBreakInspectionTest.kt
@@ -14,6 +14,29 @@ This e<weak_warning descr="Sentence does not start on a new line">tc. is missing
         myFixture.checkHighlighting()
     }

+    fun testNoWarning() {
+        myFixture.configureByText(LatexFileType, """
+            First sentence.
+            Second sentence.
+        """.trimIndent())
+        myFixture.checkHighlighting()
+    }
+
+    fun testNoWarningWithComment() {
+        myFixture.configureByText(LatexFileType, """
+            This is an abbreviation (ABC). % commemt
+            More text here.
+        """.trimIndent())
+        myFixture.checkHighlighting()
+    }
+
+    fun testNoWarningInMathMode() {
+        myFixture.configureByText(LatexFileType, """
+            \\[ Why. would. you. do. this. \\]
+        """.trimIndent())
+        myFixture.checkHighlighting()
+    }
+
     fun testQuickfix() {
         myFixture.configureByText(LatexFileType,
         """

LatexLineBreakInspection triggered incorrectly
#### Type of JetBrains IDE (IntelliJ, PyCharm, etc.) and version
IntelliJ 2020.2

#### Operating System
macOS

#### TeXiFy IDEA version
Latest alpha

#### Description
The following bit of text incorrectly triggers the `Sentence does not start on a new line` Inspection.

#### Minimal example to reproduce the problem

```latex
This is an abbreviation (ABC). % commemt
More text here.
```

package nl.hannahsten.texifyidea

import com.intellij.lang.Language

/**
 * @author Hannah Schellekens
 */
object BibtexLanguage : Language("Bibtex") {
    override fun getDisplayName(): String = "BibTeX"
}

package nl.hannahsten.texifyidea

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.TokenType
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 = WHITE_SPACES

    override fun getCommentTokens(): TokenSet = COMMENTS

    override fun getStringLiteralElements(): TokenSet = 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 WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE)
        val COMMENTS = TokenSet.create(BibtexTypes.COMMENT)
        val NORMAL_TEXT = TokenSet.create(BibtexTypes.NORMAL_TEXT)
        val FILE = object : IStubFileElementType<BibtexFileStub>(
            Language.findInstance(BibtexLanguage::class.java)
        ) {
            override fun getStubVersion(): Int = 5
        }
    }

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

package nl.hannahsten.texifyidea

import com.intellij.lang.Language

/**
 * @author Sten Wessel
 */
class LatexLanguage private constructor() : Language("Latex") {
    override fun getDisplayName(): String {
        return "LaTeX"
    }

    companion object {
        @JvmField
        val INSTANCE = LatexLanguage()
    }
}

package nl.hannahsten.texifyidea

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.TokenType
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 = WHITE_SPACES

    override fun getCommentTokens(): TokenSet = COMMENTS

    override fun getStringLiteralElements(): TokenSet = 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 {
        val WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE)
        val COMMENTS = TokenSet.create(LatexTypes.COMMENT_TOKEN)
        val NORMAL_TEXT = TokenSet.create(LatexTypes.NORMAL_TEXT)
        val FILE: IStubFileElementType<*> = object : IStubFileElementType<LatexFileStub>(
            Language.findInstance(LatexLanguage::class.java)
        ) {
            override fun getStubVersion(): Int = 8
        }
    }

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

package nl.hannahsten.texifyidea

/**
 * Exception that is thrown by problems within TeXiFy-IDEA.
 *
 * @author Hannah Schellekens
 */
class TeXception : RuntimeException {

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

package nl.hannahsten.texifyidea

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

/**
 * @author Hannah Schellekens, Sten Wessel
 */
@Suppress("unused")
object TexifyIcons {
    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val LATEX_FILE_BIG = IconLoader.getIcon(
            "/nl/hannahsten/texifyidea/icons/latex-file-big.svg"
    )

    /**
     * Copyright (c) 2017 Hannah Schellekens
     */
    val LATEX_FILE = IconLoader.getIcon(
            "/nl/hannahsten/texifyidea/icons/latex-file.svg"
    )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // From IntelliJ
    val STRING = IconLoader.getIcon(
            "/nl/hannahsten/texifyidea/icons/string.svg"
    )

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

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

    /**
     * For lookup items that have no real category.
     */
    val MISCELLANEOUS_ITEM = 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.
     * @return The Icon that corresponds to the given extension.
     * @throws IllegalArgumentException
     * When `extension` is null.
     */
    fun getIconFromExtension(extension: String?): Icon {
        return if (extension == null) {
            FILE
        }
        else when (extension.toLowerCase()) {
            "tex" -> LATEX_FILE
            "bib" -> BIBLIOGRAPHY_FILE
            "cls" -> CLASS_FILE
            "dtx" -> DOCUMENTED_LATEX_SOURCE
            "sty" -> STYLE_FILE
            "txt" -> TEXT_FILE
            "tikz" -> TIKZ_FILE
            "log" -> TEXT_FILE
            "pdf" -> PDF_FILE
            "synctex.gz" -> SYNCTEX_FILE
            else -> FILE
        }
    }
}

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
import java.io.File

/**
 * Action to delete all auxiliary files.
 *
 * @author Abby Berkers
 */
class ClearAuxFiles : 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 Magic.File.auxiliaryFileTypes }
                .forEach { it.delete() }
        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.ui.showOkCancelDialog
import com.intellij.openapi.vfs.LocalFileSystem
import nl.hannahsten.texifyidea.util.Magic
import java.io.File

/**
 * Similar to [ClearAuxFiles].
 */
class ClearGeneratedFiles : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        val project = getEventProject(e) ?: return
        val basePath = project.basePath ?: return

        // Delete generated files only in these folders
        for (folder in setOf("src", "auxil", "out")) {
            File(basePath, folder).walk().maxDepth(1)
                    .filter { it.isFile }
                    .filter { it.extension in Magic.File.generatedFileTypes }
                    .forEach { it.delete() }
        }

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

        showOkCancelDialog("Clear auxiliary and generated files",
                "Delete all LaTeX auxiliary and generated files? \n" +
                        "All auxiliary and generated files in src/, auxil/ and out/ will be deleted, \n" +
                        "including for example pdf and log files. \n" +
                        "You might not be able to fully undo this operation!",
                "Delete")
        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 javax.swing.*
import java.util.Arrays

/**
 * 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, writeAction: () -> Unit) {
        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.AnActionEvent
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.run.linuxpdfviewer.PdfViewer
import nl.hannahsten.texifyidea.settings.TexifySettings

open class ForwardSearchActionBase(val viewer: PdfViewer) : EditorAction(
        name = "_ForwardSearch",
        icon = TexifyIcons.RIGHT
) {
    override fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor) {
        if (!viewer.isAvailable()) {
            return
        }

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

        viewer.conversation!!.forwardSearch(null, file.path, line, project, focusAllowed = true)
    }

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = TexifySettings.getInstance().pdfViewer == viewer
    }
}

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 javax.swing.Icon

/**
 * @author Hannah Schellekens
 */
@Suppress("ComponentNotRegistered")
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

        runWriteAction(project) { 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.LatexPsiUtil
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.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 = LatexPsiUtil.getParentOfType(element, LatexCommands::class.java) ?: return

        runWriteAction(project) { 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 = LatexPsiUtil.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

/**
 * @author Hannah Schellekens
 */
class NewLatexFileAction : CreateElementActionBase("LaTeX File", "Create a new LaTeX file", TexifyIcons.LATEX_FILE) {
    override fun invokeDialog(project: Project, psiDirectory: PsiDirectory, elementsConsumer: java.util.function.Consumer<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)
        val consumer = com.intellij.util.Consumer<PsiElement> { }
        builder.show<PsiElement>("", null, fileCreator, consumer)
    }

    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.toLowerCase()
            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.toLowerCase()
            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

import java.util.ArrayDeque
import java.util.Deque

/**
 * Depth first search that checks if a goal node is a child of the given start node.
 *
 * @author Hannah Schellekens
 */
open class IsChildDFS<Node>(

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

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

        /**
         * Function that tests of the given node is the end node.
         */
        inline val isGoal: (Node) -> Boolean
) {

    open fun execute(): Boolean {
        // Done when starting at the goal.
        if (isGoal(start)) {
            return true
        }

        // 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)
            if (isGoal(child)) {
                return true
            }

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

        return false
    }
}

package nl.hannahsten.texifyidea.blame

import com.intellij.ide.BrowserUtil
import com.intellij.openapi.diagnostic.ErrorReportSubmitter
import com.intellij.openapi.diagnostic.IdeaLoggingEvent
import com.intellij.openapi.diagnostic.SubmittedReportInfo
import com.intellij.util.Consumer
import java.awt.Component
import java.io.UnsupportedEncodingException
import java.net.URLEncoder

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

    companion object {

        private const val URL = "https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/new?labels=crash-report&title="
        private const val ENCODING = "UTF-8"
    }

    override fun getReportActionText() = "Report to TeXiFy-IDEA issue tracker"

    override fun submit(events: Array<IdeaLoggingEvent>, additionalInfo: String?,
                        parentComponent: Component, consumer: Consumer<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(URL)
        try {
            builder.append(URLEncoder.encode(title, ENCODING))
            builder.append("&body=")

            builder.append(URLEncoder.encode("### Type of JetBrains IDE (IntelliJ, PyCharm, etc.) and version\n\n", ENCODING))
            builder.append(URLEncoder.encode("### Operating System \n" +
                    "<!-- Windows, Ubuntu, Arch Linux, MacOS, etc. -->\n\n", ENCODING))
            builder.append(URLEncoder.encode("### TeXiFy IDEA version\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(7000)}\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
    }
}

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.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexKey
import nl.hannahsten.texifyidea.util.*

/**
 * @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() ?: return
        val entryType = BibtexDefaultEntry[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) {
            Package.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.*

/**
 * @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 com.intellij.util.containers.ContainerUtil
import nl.hannahsten.texifyidea.completion.handlers.TokenTypeInsertHandler
import nl.hannahsten.texifyidea.lang.BibtexDefaultEntry
import nl.hannahsten.texifyidea.lang.Package

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

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        result.addAllElements(ContainerUtil.map2List(BibtexDefaultEntry.values()) {
            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: BibtexDefaultEntry): String {
        return " {" + entry.required.joinToString { it.toString().toLowerCase() } + "}"
    }

    private fun packageName(entry: BibtexDefaultEntry): String {
        return when (val dependency = entry.dependency) {
            Package.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.editor.ShiftTracker
import nl.hannahsten.texifyidea.file.BibtexFileType
import nl.hannahsten.texifyidea.psi.BibtexEndtry
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexId
import nl.hannahsten.texifyidea.psi.BibtexTag
import nl.hannahsten.texifyidea.psi.BibtexTypes
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.lineIndentation
import nl.hannahsten.texifyidea.util.nextSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.util.parentOfType
import nl.hannahsten.texifyidea.util.previousSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.util.repeat
import nl.hannahsten.texifyidea.util.tokenName

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

    override fun postProcessEnter(file: PsiFile, editor: Editor, context: DataContext): Result {
        ShiftTracker.setup(editor.contentComponent)
        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 || ShiftTracker.isShiftPressed()) {
            return false
        }

        // Ignore @string and @preamble
        val parent = element.parentOfType(BibtexEntry::class)
        if (parent != null) {
            val token = parent.tokenName()?.toLowerCase()
            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.util.findBibtexItems
import java.util.*

object LatexBibliographyReferenceProvider : CompletionProvider<CompletionParameters>() {
    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val lookupItems = parameters.originalFile.findBibtexItems()
                .map { bibtexEntry ->
                    when (bibtexEntry) {
                        is BibtexEntry -> {
                            val lookupStrings = LinkedList(bibtexEntry.authors)
                            lookupStrings.add(bibtexEntry.title)
                            LookupElementBuilder.create(bibtexEntry.identifier)
                                    .withPsiElement(bibtexEntry)
                                    .withPresentableText(bibtexEntry.title)
                                    .bold()
                                    .withInsertHandler(LatexReferenceInsertHandler())
                                    .withLookupStrings(lookupStrings)
                                    .withTypeText(bibtexEntry.identifier,
                                            true)
                                    .withIcon(TexifyIcons.DOT_BIB)
                        }
                        is LatexCommands -> {
                            if (bibtexEntry.requiredParameters.size == 0) return@map null
                            LookupElementBuilder.create(bibtexEntry.requiredParameters[0])
                                    .bold()
                                    .withInsertHandler(LatexReferenceInsertHandler())
                                    .withTypeText(bibtexEntry.containingFile.name + ": " + (1 + StringUtil.offsetToLineNumber(bibtexEntry.containingFile.text, bibtexEntry.textOffset)), true)
                                    .withIcon(TexifyIcons.DOT_BIB)
                        }
                        else -> {
                            null
                        }
                    }
                }
        val before = result.prefixMatcher.prefix
        val prefix =
                if (before.contains(',')) {
                    before.substring(before.lastIndexOf(',') + 1)
                }
                else {
                    before
                }
        result.withPrefixMatcher(CamelHumpMatcher(prefix, false)).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
import nl.hannahsten.texifyidea.completion.handlers.MoveToEndOfCommandHandler

/**
 * @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)
                    .withInsertHandler(MoveToEndOfCommandHandler)
        })
    }
}

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.google.common.base.Strings
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.util.text.StringUtil
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.ProcessingContext
import com.intellij.util.containers.ContainerUtil
import nl.hannahsten.texifyidea.TexifyIcons
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.LatexCommandsIndex
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.lang.*
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.Kindness.getKindWords
import nl.hannahsten.texifyidea.util.files.*
import java.util.*
import java.util.stream.Collectors

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexCommandProvider internal constructor(private val mode: LatexMode) :
    CompletionProvider<CompletionParameters>() {

    override fun addCompletions(
        parameters: CompletionParameters,
        context: ProcessingContext,
        result: CompletionResultSet
    ) {
        when (mode) {
            LatexMode.NORMAL -> {
                addNormalCommands(result)
                addCustomCommands(parameters, result)
            }
            LatexMode.MATH -> {
                addMathCommands(result)
                addCustomCommands(parameters, result, LatexMode.MATH)
            }
            LatexMode.ENVIRONMENT_NAME -> addEnvironments(result, parameters)
        }
        result.addLookupAdvertisement("Don't use \\\\ outside of tabular or math mode, it's evil.")
    }

    private fun addNormalCommands(result: CompletionResultSet) {
        result.addAllElements(LatexRegularCommand.values().flatMap { cmd ->
            cmd.arguments.toSet().optionalPowerSet().mapIndexed { index, args ->
                LookupElementBuilder.create(cmd, cmd.command + List(index) { " " }.joinToString(""))
                    .withPresentableText(cmd.commandDisplay)
                    .bold()
                    .withTailText(args.joinToString("") + " " + packageName(cmd), true)
                    .withTypeText(cmd.display)
                    .withInsertHandler(LatexNoMathInsertHandler(args.toList()))
                    .withIcon(TexifyIcons.DOT_COMMAND)
            }
        })
        result.addLookupAdvertisement(getKindWords())
    }

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

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

    private fun addEnvironments(result: CompletionResultSet, parameters: CompletionParameters) {
        // Find all environments.
        val environments: MutableList<Environment> = ArrayList()
        Collections.addAll(environments, *DefaultEnvironment.values())
        LatexDefinitionIndex.getItemsInFileSet(parameters.originalFile).stream()
            .filter { cmd -> Magic.Command.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 ->
            LookupElementBuilder.create(env, env.environmentName)
                .withPresentableText(env.environmentName)
                .bold()
                .withTailText(env.getArgumentsDisplay() + " " + packageName(env), true)
                .withIcon(TexifyIcons.DOT_ENVIRONMENT)
        })
        result.addLookupAdvertisement(getKindWords())
    }

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

    private fun addCustomCommands(
        parameters: CompletionParameters, result: CompletionResultSet,
        mode: LatexMode? = null
    ) {
        val project = parameters.editor.project ?: return
        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 searchFiles = files.stream()
            .map { obj: PsiFile -> obj.virtualFile }
            .collect(Collectors.toSet())
        searchFiles.add(file.virtualFile)
        val scope = GlobalSearchScope.filesScope(project, searchFiles)
        val cmds = LatexCommandsIndex.getItems(project, scope)
        for (cmd in cmds) {
            if (!cmd.isDefinition() && !cmd.isEnvironmentDefinition()) {
                continue
            }
            if (mode !== LatexMode.MATH && cmd.name in Magic.Command.mathCommandDefinitions) {
                continue
            }
            val cmdName = getCommandName(cmd) ?: 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\d]*])|(\{[\w\d]*})""".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 Magic.Command.commandDefinitions) {
            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.optionalParameters.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 "") + Strings.repeat("{param}", requiredParameterCount)
            }

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

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

            else -> ""
        }
    }

    private fun getCommandName(commands: LatexCommands): String? {
        return when (commands.name) {
            in Magic.Command.mathCommandDefinitions + setOf("\\newcommand", "\\newif") -> getNewCommandName(commands)
            else -> getDefinitionName(commands)
        }
    }

    private fun getNewCommandName(commands: LatexCommands) = commands.forcedFirstRequiredParameterAsCommand()?.name

    private fun getDefinitionName(commands: LatexCommands) = commands.definitionCommand()?.commandToken?.text
}

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.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.icons.AllIcons
import com.intellij.util.ProcessingContext
import com.intellij.util.containers.ContainerUtil
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.ALL_TEXIFY_INSPECTIONS

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

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val file = parameters.originalFile
        val inspectionIds = InsightGroup.byFileType(file.fileType)
                .flatMap { ALL_TEXIFY_INSPECTIONS[it] ?: emptyList() }

        result.addAllElements(ContainerUtil.map2List(inspectionIds) {
            LookupElementBuilder.create(it, it)
                    .withPresentableText(it)
                    .bold()
                    .withIcon(AllIcons.General.InspectionsEye)
        })
    }
}

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 com.intellij.util.containers.ContainerUtil
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(ContainerUtil.map2List(keys) {
            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

            document.insertString(offset, " = ")
            caret.moveToOffset(offset + 3)
        }
    }
}

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

class LatexMagicCommentValueProvider(val values: HashSet<String>) : CompletionProvider<CompletionParameters>() {

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        result.addAllElements(values.map {
            LookupElementBuilder.create(it)
        })
    }
}

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.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.util.PackageUtils
import nl.hannahsten.texifyidea.util.projectSearchScope
import nl.hannahsten.texifyidea.util.requiredParameter

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

    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
        val packageNames = PackageUtils.CTAN_PACKAGE_NAMES.toMutableSet()
        val project = parameters.originalFile.project
        val customPackages = LatexDefinitionIndex.getCommandsByName("\\ProvidesPackage", project, project.projectSearchScope)
        packageNames.addAll(customPackages.mapNotNull { it.requiredParameter(0)?.trim() })

        result.addAllElements(ContainerUtil.map2List(packageNames) { 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.psi.search.GlobalSearchScope
import com.intellij.util.ProcessingContext
import com.intellij.util.ui.ColorIcon
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.insight.LatexElementColorProvider
import nl.hannahsten.texifyidea.util.Kindness
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import nl.hannahsten.texifyidea.util.getRequiredArgumentValueByName
import nl.hannahsten.texifyidea.util.isColorDefinition
import java.awt.Color
import java.util.*
import java.util.stream.Collectors

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(
                Magic.Colors.defaultXcolors.map {
                    LookupElementBuilder.create(it.key)
                            .withIcon(ColorIcon(12, Color(it.value)))
                }
        )
    }

    private fun addCustomColors(parameters: CompletionParameters, result: CompletionResultSet) {
        val project = parameters.editor.project ?: return
        val file = parameters.originalFile
        val files: MutableSet<PsiFile> = HashSet(file.referencedFileSet())
        val searchFiles = files.stream()
                .map { obj: PsiFile -> obj.virtualFile }
                .collect(Collectors.toSet())
        searchFiles.add(file.virtualFile)
        val scope = GlobalSearchScope.filesScope(project, searchFiles)
        val cmds = LatexCommandsIndex.getItems(project, scope)
        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.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.patterns.PatternCondition
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.util.ProcessingContext
import nl.hannahsten.texifyidea.BibtexLanguage
import nl.hannahsten.texifyidea.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.lang.*
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.psi.LatexMathEnvironment
import nl.hannahsten.texifyidea.run.compiler.BibliographyCompiler
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import nl.hannahsten.texifyidea.util.*
import java.util.EnumSet

/**
 * 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 TexifyCompletionContributor : CompletionContributor() {

    init {
        registerLatexCompletion()
        registerBibtexCompletion()
    }

    private fun registerLatexCompletion() {
        // Math mode
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement(LatexTypes.COMMAND_TOKEN)
                        .inside(LatexMathEnvironment::class.java)
                        .withLanguage(LatexLanguage.INSTANCE),
                LatexCommandProvider(LatexMode.MATH)
        )

        // Math mode inside environments
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement(LatexTypes.COMMAND_TOKEN)
                        .with(object : PatternCondition<PsiElement>(null) {
                            override fun accepts(psiElement: PsiElement, processingContext: ProcessingContext): Boolean {
                                return psiElement.inMathContext()
                            }
                        })
                        .withLanguage(LatexLanguage.INSTANCE),
                LatexCommandProvider(LatexMode.MATH)
        )

        // Normal
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement(LatexTypes.COMMAND_TOKEN)
                        .andNot(PlatformPatterns.psiElement()
                                .inside(LatexMathEnvironment::class.java))
                        .with(object : PatternCondition<PsiElement>(null) {
                            override fun accepts(psiElement: PsiElement, processingContext: ProcessingContext): Boolean {
                                return !psiElement.inMathContext()
                            }
                        })
                        .withLanguage(LatexLanguage.INSTANCE),
                LatexCommandProvider(LatexMode.NORMAL)
        )

        // DefaultEnvironment names
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement()
                        .inside(LatexRequiredParam::class.java)
                        .inside(LatexBeginCommand::class.java)
                        .withLanguage(LatexLanguage.INSTANCE),
                LatexCommandProvider(LatexMode.ENVIRONMENT_NAME)
        )

        // File names
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement().inside(LatexParameterText::class.java)
                        .inside(LatexRequiredParam::class.java)
                        .with(object : PatternCondition<PsiElement>("File name completion pattern") {
                            override fun accepts(psiElement: PsiElement, processingContext: ProcessingContext): Boolean {
                                val command = LatexPsiUtil.getParentOfType(psiElement, LatexCommands::class.java)
                                        ?: return false

                                val name = command.commandToken.text
                                val cmd = LatexRegularCommand[name.substring(1)] ?: return false

                                val args = cmd.first().getArgumentsOf(RequiredFileArgument::class)
                                if (args.isNotEmpty()) processingContext.put("type", args.first())

                                return args.isNotEmpty()
                            }
                        })
                        .withLanguage(LatexLanguage.INSTANCE),
                LatexFileProvider()
        )

        // Folder names
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement()
                        .inside(LatexRequiredParam::class.java)
                        .with(object : PatternCondition<PsiElement>("Folder name completion pattern") {
                            override fun accepts(psiElement: PsiElement, processingContext: ProcessingContext): Boolean {
                                val command = LatexPsiUtil.getParentOfType(psiElement, LatexCommands::class.java)
                                        ?: return false

                                val name = command.commandToken.text
                                val cmd = LatexRegularCommand[name.substring(1)] ?: return false

                                val args = cmd.first().getArgumentsOf(RequiredFolderArgument::class)
                                if (args.isNotEmpty()) processingContext.put("type", args.first())

                                return args.isNotEmpty()
                            }
                        })
                        .withLanguage(LatexLanguage.INSTANCE),
                LatexFolderProvider()
        )

        // Graphics paths
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement().inside(LatexParameterText::class.java)
                        .inside(LatexRequiredParam::class.java)
                        .with(object : PatternCondition<PsiElement>("Folder name completion pattern") {
                            override fun accepts(psiElement: PsiElement, processingContext: ProcessingContext): Boolean {
                                val command = LatexPsiUtil.getParentOfType(psiElement, LatexCommands::class.java)
                                        ?: return false

                                val name = command.commandToken.text
                                val cmd = LatexRegularCommand[name.substring(1)] ?: return false

                                val args = cmd.first().getArgumentsOf(RequiredPicturePathArgument::class)
                                if (args.isNotEmpty()) processingContext.put("type", args.first())

                                return args.isNotEmpty()
                            }
                        })
                        .withLanguage(LatexLanguage.INSTANCE),
                LatexGraphicsPathProvider()
        )

        // Colors from xcolor
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement().inside(LatexRequiredParam::class.java)
                        .with(object : PatternCondition<PsiElement>("xcolor color completion patter") {
                            override fun accepts(psiElement: PsiElement, context: ProcessingContext?): Boolean {
                                val command = LatexPsiUtil.getParentOfType(psiElement, LatexCommands::class.java)
                                        ?: return false

                                val name = command.commandToken.text
                                return name.substring(1) in Magic.Colors.takeColorCommands
                            }
                        }),
                LatexXColorProvider
        )

        // Magic comments keys.
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement().inside(PsiComment::class.java)
                        .with(object : PatternCondition<PsiElement>("Magic comment completion pattern") {
                            override fun accepts(comment: PsiElement, context: ProcessingContext?): Boolean {
                                return comment.isMagicComment() && comment.text.contains('=').not()
                            }
                        })
                        .withLanguage(LatexLanguage.INSTANCE),
                LatexMagicCommentKeyProvider
        )

        extendLatexCommands(LatexBibliographyReferenceProvider, Magic.Command.bibliographyReference)

        // Inspection list for magic comment suppress
        val suppressRegex = Regex("""suppress\s*=\s*""", EnumSet.of(RegexOption.IGNORE_CASE))
        extendMagicCommentValues("suppress", suppressRegex, LatexInspectionIdProvider)

        // List containing tikz/math to autocomplete the begin/end/preamble values in magic comments
        val beginEndRegex = Regex("""(begin|end|preview) preamble\s*=\s*""", EnumSet.of(RegexOption.IGNORE_CASE))
        extendMagicCommentValues("preamble", beginEndRegex, LatexMagicCommentValueProvider(Magic.Comment.preambleValues))

        // List of LaTeX compilers
        val compilerRegex = Regex("""compiler\s*=\s*""", EnumSet.of(RegexOption.IGNORE_CASE))
        extendMagicCommentValues("compiler", compilerRegex, LatexMagicCommentValueProvider(LatexCompiler.values().map { it.executableName }.toHashSet()))

        val bibtexCompilerRegex = Regex("""bibtex compiler\s*=\s*""", EnumSet.of(RegexOption.IGNORE_CASE))
        extendMagicCommentValues("bibtex compiler", bibtexCompilerRegex, LatexMagicCommentValueProvider(BibliographyCompiler.values().map { it.executableName }.toHashSet()))

        // Package names
        extendLatexCommands(LatexPackageNameProvider, "\\usepackage", "\\RequirePackage")

        // Documentclasses
        extendLatexCommands(LatexDocumentclassProvider,
                "\\documentclass", "\\LoadClass", "\\LoadClassWithOptions"
        )

        // Bibliography styles
        extendLatexCommand(LatexBibliographyStyleProvider, "\\bibliographystyle")
    }

    private fun registerBibtexCompletion() {
        // Outer scope: types.
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement(BibtexTypes.TYPE_TOKEN)
                        .andNot(PlatformPatterns.psiElement().inside(BibtexEntry::class.java))
                        .withLanguage(BibtexLanguage),
                BibtexTypeTokenProvider
        )

        // Keys
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement(BibtexTypes.IDENTIFIER)
                        .inside(BibtexEntry::class.java)
                        .with(object : PatternCondition<PsiElement>(null) {
                            override fun accepts(psiElement: PsiElement, context: ProcessingContext): Boolean {
                                val entry = psiElement.parentOfType(BibtexEntry::class)
                                val type = entry?.firstChildOfType(BibtexType::class)
                                if (type?.text?.toLowerCase() == "@string") {
                                    return false
                                }

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

        // Strings
        extend(
                CompletionType.BASIC,
                PlatformPatterns.psiElement(BibtexTypes.IDENTIFIER)
                        .inside(BibtexEntry::class.java)
                        .inside(BibtexContent::class.java)
                        .withLanguage(BibtexLanguage),
                BibtexStringProvider
        )
    }

    /**
     * Adds a completion contributor that gets activated within the first required parameter of a given command.
     */
    private fun extendLatexCommand(provider: CompletionProvider<CompletionParameters>, commandNameWithSlash: String) {
        extendLatexCommands(provider, setOf(commandNameWithSlash))
    }

    /**
     * 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)
                        .with(object : PatternCondition<PsiElement>(null) {
                            override fun accepts(psiElement: PsiElement, context: ProcessingContext): Boolean {
                                val command = psiElement.parentOfType(LatexCommands::class) ?: return false
                                if (command.commandToken.text in commandNamesWithSlash) {
                                    return true
                                }

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

    /**
     * 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(PsiComment::class.java)
                        .with(object : PatternCondition<PsiElement>("Magic comment $commentName pattern") {
                            override fun accepts(comment: PsiElement, context: ProcessingContext?): Boolean {
                                return comment.isMagicComment() && comment.text.contains(regex)
                            }
                        })
                        .withLanguage(LatexLanguage.INSTANCE),
                completionProvider
        )
    }
}

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.LatexCommands
import nl.hannahsten.texifyidea.util.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 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.lang.LatexCommand
import nl.hannahsten.texifyidea.lang.Package
import nl.hannahsten.texifyidea.lang.Package.Companion.DEFAULT
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexId
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.run.latex.LatexDistribution
import nl.hannahsten.texifyidea.util.parentsOfType
import nl.hannahsten.texifyidea.util.previousSiblingIgnoreWhitespace
import java.io.IOException
import java.io.InputStream

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

    companion object {
        private val PACKAGE_COMMANDS = setOf("usepackage", "RequirePackage", "documentclass", "LoadClass")
    }

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

    override fun getUrlFor(element: PsiElement?, originalElement: PsiElement?): List<String>? {
        if (element !is LatexCommands) {
            return null
        }

        val command = LatexCommand.lookup(element) ?: return null

        // Special case for package inclusion commands
        if (command.first().command in PACKAGE_COMMANDS) {
            val pkg = element.requiredParameters.getOrNull(0) ?: return null
            return runTexdoc(Package(pkg))
        }

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

    // 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) {
            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.title} (${entry.year})
")
            stringBuilder.append(entry.authors.joinToString(", ") { a -> formatAuthor(a) })
            stringBuilder.append("

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

        if (element.previousSiblingIgnoreWhitespace() != null) {
            return lookup?.description
        }
        else lookup = null

        originalElement ?: return null
        val urls = getUrlFor(element, originalElement) ?: return null

        if (urls.isEmpty()) {
            return null
        }

        val sb = StringBuilder("
External package documentation
")
        for (url in urls) {
            sb.append("$url
")
        }

        return sb.toString()
    }

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

        lookup = obj
        return psiElement
    }

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

    private fun runTexdoc(pkg: Package): List<String> {
        val name = if (pkg == DEFAULT) "source2e" else pkg.name

        val stream: InputStream
        try {
            // -M to avoid texdoc asking to choose from the list
            val command = if (LatexDistribution.isTexliveAvailable) {
                "texdoc -l -M $name"
            }
            else {
                // texdoc on MiKTeX is just a shortcut for mthelp which doesn't need the -M option
                "texdoc -l $name"
            }
            stream = Runtime.getRuntime().exec(command).inputStream
        }
        catch (e: IOException) {
            return emptyList()
        }

        val lines = stream.bufferedReader().use { it.readLines() }

        return if (lines.getOrNull(0)?.endsWith("could not be found.") == true) {
            emptyList()
        }
        else {
            if (LatexDistribution.isTexliveAvailable) {
                lines.map {
                    // Line consists of: name version path optional file description
                    it.split("\t")[2]
                }
            }
            else {
                lines
            }
        }
    }
}

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.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

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.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.codeInsight.editorActions.BackspaceHandlerDelegate
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiFile
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

class InlineMathBackspaceHandler : BackspaceHandlerDelegate() {
    override fun beforeCharDeleted(c: Char, file: PsiFile, editor: Editor) {
        if (c == '$' && file.isLatexFile()) {
            val offset = editor.caretOffset()
            if (file.document()?.get(offset) == "$") {
                file.document()?.deleteString(offset, offset + 1)
            }
        }
    }

    override fun charDeleted(c: Char, file: PsiFile, editor: Editor): Boolean {
        return true
    }
}

package nl.hannahsten.texifyidea.editor

import com.intellij.codeInsight.editorActions.AutoHardWrapHandler
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate.Result
import com.intellij.ide.impl.DataManagerImpl
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 nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexOptionalParam
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.*

/**
 * @author Hannah Schellekens
 */
class InsertEnumerationItem : 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
        // Unfortunately the only way to get to the userData in the DataContext seems to be the deprecated MyDataContext
        if ((context as? DataManagerImpl.MyDataContext)?.getUserData(AutoHardWrapHandler.AUTO_WRAP_LINE_IN_PROGRESS_KEY) == true) {
            return Result.Continue
        }

        ShiftTracker.setup(editor.contentComponent)
        ControlTracker.setup(editor.contentComponent)

        val caret = editor.caretModel
        val element = file.findElementAt(caret.offset)
        if (hasValidContext(element)) {
            editor.insertAndMove(caret.offset, getInsertionString(element!!))
        }
        else {
            if (ControlTracker.isControlPressed) {
                editor.insertAndMove(caret.offset, "")
            }
        }

        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 string that must be inserted at the caret.
     */
    private fun getInsertionString(element: PsiElement): String {
        val marker = getPreviousMarker(element)
        return "\\item" + if (marker == null) " " else "$marker "
    }

    /**
     * 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? {
        val commands = environment.childrenOfType(LatexCommands::class).filter { it.name == "\\item" }
        if (commands.isEmpty()) {
            return null
        }

        return commands.last()
    }

    /**
     * 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?): Boolean {
        if (!TexifySettings.getInstance().automaticItemInItemize || element == null || ShiftTracker.isShiftPressed() || ControlTracker.isControlPressed || element.inMathContext()) {
            return false
        }

        val isGluedToTheBeginCommand = element.hasParent(LatexBeginCommand::class)
        val isInsideAnEnumeration = element.inDirectEnvironment(Magic.Environment.listingEnvironments)
        return isInsideAnEnumeration && !isGluedToTheBeginCommand
    }
}

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.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?.map { it as PsiElement }?.toArray(emptyArray()) ?: emptyArray()
    }
}

package nl.hannahsten.texifyidea.editor

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.LatexRegularCommand
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.getOpenAndCloseQuotes
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

        // 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))

            // 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(LatexRegularCommand.ENQUOTE.dependency)
            }
            else {
                file.insertUsepackage(LatexRegularCommand.ENQUOTE_STAR.dependency)
            }
        }
    }
}

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.Package
import nl.hannahsten.texifyidea.util.*

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"
                Magic.Pattern.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) && Package.AMSMATH.name !in file.includedPackages()) {
                file.insertUsepackage(Package.AMSMATH)
            }
        }
    }

    private fun isAmsMathEnvironment(environmentName: String): Boolean {
        val amsMathEnvironments: Array<String> = DefaultEnvironment.values()
                .filter { it.dependency == Package.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

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(Magic.Pattern.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.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.*

/**
 * @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.parent.parent ?: 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

import com.intellij.openapi.fileTypes.LanguageFileType
import nl.hannahsten.texifyidea.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
}

package nl.hannahsten.texifyidea.file

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

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

    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.LatexLanguage
import nl.hannahsten.texifyidea.TexifyIcons

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

    override fun getName() = "LaTeX class file"

    override fun getDescription() = "LaTeX class file"

    override fun getDefaultExtension() = "cls"

    override fun getIcon() = TexifyIcons.CLASS_FILE
}

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 nl.hannahsten.texifyidea.LatexLanguage

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

    override fun getFileType() = LatexFileType

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

package nl.hannahsten.texifyidea.file

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

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

    override fun getName() = "LaTeX source file"

    override fun getDescription() = "LaTeX source file"

    override fun getDefaultExtension() = "tex"

    override fun getIcon() = TexifyIcons.LATEX_FILE
}

package nl.hannahsten.texifyidea.file

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

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

    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.LatexLanguage
import nl.hannahsten.texifyidea.TexifyIcons

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

    override fun getName() = "LaTeX style file"

    override fun getDescription() = "LaTeX style file"

    override fun getDefaultExtension() = "sty"

    override fun getIcon() = TexifyIcons.STYLE_FILE
}

package nl.hannahsten.texifyidea.file

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

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

    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.LatexLanguage
import nl.hannahsten.texifyidea.TexifyIcons

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

    override fun getName() = "TikZ picture file"

    override fun getDescription() = "TikZ picture file"

    override fun getDefaultExtension() = "tikz"

    override fun getIcon() = TexifyIcons.TIKZ_FILE
}

package nl.hannahsten.texifyidea.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.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexTypes
import nl.hannahsten.texifyidea.util.childrenOfType
import java.util.*

/**
 * Adds folding regions for BibTeX entries.
 *
 * @author Thomas Schouten
 */
class BibtexEntryFoldingBuilder : FoldingBuilderEx() {

    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.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.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.childrenOfType
import nl.hannahsten.texifyidea.util.endOffset
import java.util.*

/**
 * Adds folding regions for LaTeX environments.
 *
 * Enables folding of `\begin{environment} ... \end{environment}`.
 *
 * @author Sten Wessel
 */
class LatexEnvironmentFoldingBuilder : FoldingBuilderEx() {

    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.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.psi.PsiElement
import nl.hannahsten.texifyidea.util.files.commandsInFile

/**
 * @author Johannes Berger
 */
class LatexEscapedSymbolFoldingBuilder : FoldingBuilderEx() {

    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.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.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexContent
import nl.hannahsten.texifyidea.psi.PsiContainer
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.firstChildOfType
import nl.hannahsten.texifyidea.util.parentOfType

/**
 * Folds multiple \\usepackage or \\RequirePackage statements
 *
 * @author Hannah Schellekens
 */
open class LatexImportFoldingBuilder : FoldingBuilderEx() {

    companion object {

        private val includesSet = setOf("\\usepackage", "\\RequirePackage")
        private val includesArray = includesSet.toTypedArray()
    }

    override fun isCollapsedByDefault(node: ASTNode) = true

    override fun getPlaceholderText(node: ASTNode) = "\\usepackage{...}"

    override fun buildFoldRegions(root: PsiElement, document: Document, quick: Boolean): Array<FoldingDescriptor> {
        val descriptors = ArrayList<FoldingDescriptor>()
        val covered = HashSet<LatexCommands>()
        val commands = LatexIncludesIndex.getCommandsByNames(root.containingFile, *includesArray)

        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()
            }

            val elt = PsiContainer(command, last)
            descriptors.add(FoldingDescriptor(elt, elt.textRange))
        }

        return descriptors.toTypedArray()
    }

    private fun PsiElement.nextCommand(): LatexCommands? {
        val content = if (this is LatexCommands) {
            parentOfType(LatexContent::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 (Magic.Pattern.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.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.psi.PsiElement
import nl.hannahsten.texifyidea.lang.LatexMathCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexMathEnvironment
import nl.hannahsten.texifyidea.util.childrenOfType

/**
 * @author Sten Wessel
 */
class LatexMathSymbolFoldingBuilder : FoldingBuilderEx() {

    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.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.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.psi.LatexContent
import nl.hannahsten.texifyidea.util.nextSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.util.parentOfType
import nl.hannahsten.texifyidea.util.previousSiblingIgnoreWhitespace

/**
 * Recursively folds section commands
 *
 * @author Tom Evers
 */
open class LatexSectionFoldingBuilder : FoldingBuilderEx() {

    companion object {
        private val sectionCommands = arrayOf(
                "\\part", "\\chapter",
                "\\section", "\\subsection", "\\subsubsection",
                "\\paragraph", "\\subparagraph"
            )
    }

    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 = LatexCommandsIndex.getCommandsByNames(root.containingFile, *sectionCommands).toList()
                .sortedBy { it.textOffset }
        if (commands.isEmpty()) {
            return descriptors.toTypedArray()
        }

        for (currentFoldingCommandIndex in commands.indices) {
            var foundHigherCommand = false
            val currentFoldingCommand = commands[currentFoldingCommandIndex]
            val currentCommandRank = sectionCommands.indexOf(currentFoldingCommand.name)

            // 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 commands.size) {
                val nextFoldingCommand = commands[nextFoldingCommandIndex]

                // Find the rank of the next command to compare with the current rank
                val nextCommandRank = sectionCommands.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(LatexContent::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 = commands.last().parentOfType(LatexContent::class) ?: continue
                var nextContent: LatexContent? = foldingContent
                var previousContent: LatexContent = foldingContent
                while (nextContent != null) {
                    previousContent = nextContent
                    nextContent = nextContent.nextSiblingIgnoreWhitespace() as? LatexContent
                }
                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.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.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleSettings
import nl.hannahsten.texifyidea.BibtexLanguage
import nl.hannahsten.texifyidea.psi.BibtexTypes

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

    private fun createSpacingBuilder(settings: CodeStyleSettings): SpacingBuilder {
        val spacing = SpacingBuilder(settings, BibtexLanguage)
        spacing.around(BibtexTypes.ASSIGNMENT).spaces(1)
        spacing.before(BibtexTypes.SEPARATOR).spaces(0)
        spacing.between(BibtexTypes.TYPE, BibtexTypes.OPEN_BRACE).spaces(0)
        spacing.between(BibtexTypes.TYPE, BibtexTypes.OPEN_PARENTHESIS).spaces(0)
        spacing.between(BibtexTypes.OPEN_BRACE, BibtexTypes.ID).spaces(0)
        spacing.after(BibtexTypes.OPEN_PARENTHESIS).spaces(1)
        spacing.around(BibtexTypes.CONCATENATE).spaces(1)
        spacing.between(BibtexTypes.NORMAL_TEXT_WORD, BibtexTypes.NORMAL_TEXT_WORD).spaces(1)
        spacing.between(BibtexTypes.ENTRY_CONTENT, BibtexTypes.ENDTRY).spaces(1)
        return spacing
    }

    override fun createModel(element: PsiElement, settings: CodeStyleSettings) = FormattingModelProvider.createFormattingModelForPsiFile(
            element.containingFile,
            BibtexBlock(
                    element.node,
                    Wrap.createWrap(WrapType.NONE, false),
                    Alignment.createAlignment(),
                    createBibtexSpacingBuilder(settings)
            ),
            settings
    )!!

    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.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)
        }

        custom {
            // Only insert a space between two actual words, so when the left word
            // is not a left brace, and the right word is not a right brace.
            customRule { _, left, right ->
                return@customRule if (right.node?.elementType === NORMAL_TEXT_WORD && left.node?.elementType === NORMAL_TEXT_WORD) {
                    if (left.node?.text == "{" || right.node?.text == "}") 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.formatting.*
import com.intellij.lang.ASTNode
import com.intellij.psi.TokenType
import com.intellij.psi.formatter.common.AbstractBlock
import nl.hannahsten.texifyidea.psi.LatexTypes
import java.util.ArrayList

/**
 * @author Sten Wessel
 */
class LatexBlock(
        node: ASTNode,
        wrap: Wrap?,
        alignment: Alignment?,
        private val spacingBuilder: TexSpacingBuilder,
        private val wrappingStrategy: LatexWrappingStrategy
) : AbstractBlock(node, wrap, alignment) {

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

        while (child != null) {
            if (child.elementType !== TokenType.WHITE_SPACE) {
                val block: Block = LatexBlock(
                        child,
                        wrappingStrategy.getWrap(),
                        null,
                        spacingBuilder,
                        wrappingStrategy
                )
                blocks.add(block)
            }
            child = child.treeNext
        }
        return blocks
    }

    override fun getIndent(): Indent? {
        if (myNode.elementType === LatexTypes.ENVIRONMENT_CONTENT ||
                myNode.elementType === LatexTypes.PSEUDOCODE_BLOCK_CONTENT ||
                // Fix for leading comments inside an environment, because
                // somehow they are not placed inside environments.
                myNode.elementType === LatexTypes.COMMENT_TOKEN &&
                myNode.treeParent.elementType === LatexTypes.ENVIRONMENT) {
            return Indent.getNormalIndent(true)
        }

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

    override fun getChildAttributes(newChildIndex: Int): ChildAttributes {
        val type = myNode.elementType
        if (type === LatexTypes.DISPLAY_MATH) {
            return ChildAttributes(Indent.getNormalIndent(true), null)
        }
        else if (type === LatexTypes.ENVIRONMENT) {
            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.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleSettings

/**
 * @author Sten Wessel
 */
class LatexFormattingModelBuilder : FormattingModelBuilder {
    override fun createModel(element: PsiElement, settings: CodeStyleSettings): FormattingModel {
        return FormattingModelProvider.createFormattingModelForPsiFile(
                element.containingFile,
                LatexBlock(
                        element.node,
                        Wrap.createWrap(WrapType.NONE, false),
                        Alignment.createAlignment(),
                        createSpacingBuilder(settings),
                        LatexWrappingStrategy()
                ),
                settings
        )
    }

    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

/**
 * @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) {
            Magic.Environment.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.LatexLanguage
import nl.hannahsten.texifyidea.formatting.spacingrules.leftTableSpaceAlign
import nl.hannahsten.texifyidea.formatting.spacingrules.rightTableSpaceAlign
import nl.hannahsten.texifyidea.psi.LatexTypes.*
import nl.hannahsten.texifyidea.settings.codestyle.LatexCodeStyleSettings
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.inDirectEnvironment

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.INSTANCE)

    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(Magic.Environment.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(Magic.Environment.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?.text?.toLowerCase() in setOf("\\state", "\\statex") && parent.node?.psi?.inDirectEnvironment(Magic.Environment.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? Does not work for a command that immediately follows
            // \begin{document}. But no one should start their document like
            // that anyway.
            customRule { _, _, right ->
                LatexCodeStyleSettings.blankLinesOptions.forEach {
                    if (right.node?.text?.matches(Regex("\\" + "${it.value}\\{.*\\}")) == true) {
                        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.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].
     */
    @Suppress("RedundantOverride") // Abstract method has to be overridden
    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.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 com.intellij.util.Function
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.LatexBeginCommand
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.isEntryPoint
import nl.hannahsten.texifyidea.util.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? {
        if (element.firstChild != null || element.parent !is LatexParameterText) return null

        val beginCommand = element.parentOfType(LatexBeginCommand::class) ?: return null

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

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

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

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.LatexCommand
import nl.hannahsten.texifyidea.lang.RequiredFileArgument
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexRequiredParamContent
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.files.getFileExtension
import nl.hannahsten.texifyidea.util.parentOfType
import nl.hannahsten.texifyidea.util.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.firstOrNull()?.getArgumentsOf(RequiredFileArgument::class.java)
        if (arguments?.isNullOrEmpty() == true) {
            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 extension = if (files.isNotEmpty()) files.first().name.getFileExtension() else ""
        val icon = TexifyIcons.getIconFromExtension(extension)

        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))
    }

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

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

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.BibtexEntry
import nl.hannahsten.texifyidea.psi.BibtexKey
import nl.hannahsten.texifyidea.psi.BibtexQuotedString
import nl.hannahsten.texifyidea.util.parentOfType
import nl.hannahsten.texifyidea.util.tokenType

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

    override fun annotate(element: PsiElement, holder: AnnotationHolder) {
        when (element) {
            is BibtexBracedString -> annotate(element, holder)
            is BibtexQuotedString -> 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 "Quoted Strings".
     */
    private fun annotate(quotedString: BibtexQuotedString, holder: AnnotationHolder) {
        holder.newAnnotation(HighlightSeverity.INFORMATION, "")
                .range(quotedString)
                .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()?.toLowerCase()
        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.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 -> IDENTIFIER_KEYS
        BibtexTypes.KEY -> KEY_KEYS
        BibtexTypes.NUMBER -> NUMBER_KEYS
        BibtexTypes.TYPE_TOKEN -> TYPE_TOKEN_KEYS
        BibtexTypes.STRING, BibtexTypes.QUOTED_STRING -> STRING_KEYS
        BibtexTypes.CONTENT, BibtexTypes.BRACED_STRING -> 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.PsiComment
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.*
import nl.hannahsten.texifyidea.util.files.definitionsAndRedefinitionsInFileSet

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

    companion object {

        /**
         * The maximum amount of times the cache may be used before doing another lookup.
         */
        private const val MAX_CACHE_COUNT = 40
    }

    // Cache to prevent many PsiFile#isUsed and PsiFile#definitions() lookups.
    private var definitionCache: Collection<LatexCommands>? = null
    private var definitionCacheFile: PsiFile? = null
    private var definitionCacheCount: Int = 0

    /**
     * Looks up all the definitions in the file set.
     *
     * It does cache all the definitions for [MAX_CACHE_COUNT] lookups.
     * See also members [definitionCache], [definitionCacheFile], and [definitionCacheCount]
     */
    private fun PsiFile.definitionCache(): Collection<LatexCommands> {
        // Initialise.
        if (definitionCache == null) {
            definitionCache = definitionsAndRedefinitionsInFileSet().filter { it.isEnvironmentDefinition() }
            definitionCacheFile = this
            definitionCacheCount = 0
            return definitionCache!!
        }

        // Check if the file is the same.
        if (definitionCacheFile != this) {
            definitionCache = definitionsAndRedefinitionsInFileSet().filter { it.isEnvironmentDefinition() }
            definitionCacheCount = 0
            definitionCacheFile = this
        }

        // Re-evaluate after the count has been reached times.
        if (++definitionCacheCount > MAX_CACHE_COUNT) {
            definitionCache = definitionsAndRedefinitionsInFileSet().filter { it.isEnvironmentDefinition() }
            definitionCacheCount = 0
        }

        return definitionCache!!
    }

    override fun annotate(psiElement: PsiElement, annotationHolder: AnnotationHolder) {
        // Comments
        if (psiElement is PsiComment) {
            annotateComment(psiElement, annotationHolder)
        }
        else if (psiElement.inDirectEnvironmentContext(Environment.Context.COMMENT)) {
            annotateComment(psiElement, annotationHolder)
        }
        // Math display
        else 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.endOffset(), 4))
                        .textAttributes(LatexSyntaxHighlighter.COMMAND_MATH_DISPLAY)
                        .create()
            }
        }
        // Optional parameters
        else if (psiElement is LatexOptionalParam) {
            annotateOptionalParameters(psiElement, annotationHolder)
        }
        // Commands
        else if (psiElement is LatexCommands) {
            annotateCommands(psiElement, annotationHolder)
        }
    }

    /**
     * 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.newAnnotation(HighlightSeverity.INFORMATION, "")
                .range(inlineMathElement)
                .textAttributes(LatexSyntaxHighlighter.INLINE_MATH)
                .create()

        annotateMathCommands(LatexPsiUtil.getAllChildren(inlineMathElement), 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.newAnnotation(HighlightSeverity.INFORMATION, "")
                .range(displayMathElement)
                .textAttributes(LatexSyntaxHighlighter.DISPLAY_MATH)
                .create()

        annotateMathCommands(displayMathElement.childrenOfType(LatexCommands::class), annotationHolder,
                LatexSyntaxHighlighter.COMMAND_MATH_DISPLAY)
    }

    /**
     * Annotates the given comment.
     */
    private fun annotateComment(comment: PsiElement, annotationHolder: AnnotationHolder) {
        val file = comment.containingFile
        val hasDefinition = file.definitionCache().any { it.requiredParameter(0) == "comment" }
        if (hasDefinition) {
            return
        }

        val textAttributes = if (comment.isMagicComment()) {
            LatexSyntaxHighlighter.MAGIC_COMMENT
        }
        else LatexSyntaxHighlighter.COMMENT

        annotationHolder.newAnnotation(HighlightSeverity.INFORMATION, "")
                .range(comment)
                .textAttributes(textAttributes)
                .create()
    }

    /**
     * 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.newAnnotation(HighlightSeverity.INFORMATION, "")
                    .range(token)
                    .textAttributes(highlighter)
                    .create()

            if (element.name == "\\text" || element.name == "\\intertext") {
                annotationHolder.newAnnotation(HighlightSeverity.INFORMATION, "")
                        .range(element.requiredParameters().firstOrNull() ?: continue)
                        .textAttributes(LatexSyntaxHighlighter.MATH_NESTED_TEXT)
                        .create()
            }
        }
    }

    /**
     * Annotates the given optional parameters of commands.
     */
    private fun annotateOptionalParameters(optionalParamElement: LatexOptionalParam,
                                           annotationHolder: AnnotationHolder) {
        for (element in optionalParamElement.optionalParamContentList
        ) {
            if (element !is LatexOptionalParamContent) {
                continue
            }

            val toStyle = element.parameterText ?: continue

            annotationHolder.newAnnotation(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)

        // Label references.
        val style = when (command.name) {
            in Magic.Command.labelReferenceWithoutCustomCommands -> {
                LatexSyntaxHighlighter.LABEL_REFERENCE
            }
            // Label definitions.
            in Magic.Command.getLabelDefinitionCommands() -> {
                LatexSyntaxHighlighter.LABEL_DEFINITION
            }
            // Bibliography references (citations).
            in Magic.Command.bibliographyReference -> {
                LatexSyntaxHighlighter.BIBLIOGRAPHY_REFERENCE
            }
            // Label definitions.
            in Magic.Command.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" -> LatexSyntaxHighlighter.STYLE_BOLD
            "\\textit" -> LatexSyntaxHighlighter.STYLE_ITALIC
            "\\underline" -> LatexSyntaxHighlighter.STYLE_UNDERLINE
            "\\sout" -> LatexSyntaxHighlighter.STYLE_STRIKETHROUGH
            "\\textsc" -> LatexSyntaxHighlighter.STYLE_SMALL_CAPITALS
            "\\overline" -> LatexSyntaxHighlighter.STYLE_OVERLINE
            "\\texttt" -> LatexSyntaxHighlighter.STYLE_TYPEWRITER
            "\\textsl" -> LatexSyntaxHighlighter.STYLE_SLANTED
            else -> return
        }

        command.requiredParameters().firstOrNull()?.let {
            annotationHolder.annotateRequiredParameter(it, style)
        }
    }

    /**
     * Annotates the contents of the given parameter with the given style.
     */
    private fun AnnotationHolder.annotateRequiredParameter(parameter: LatexRequiredParam, style: TextAttributesKey) {
        val content = parameter.firstChildOfType(LatexContent::class) ?: return
        this.newAnnotation(HighlightSeverity.INFORMATION, "")
                .range(content)
                .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//Commands", LatexSyntaxHighlighter.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,
                "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,
                "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}
                |
                |% 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>}.
                |
                |    \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>}
                |    \underline{<styleUnderline>Underline</styleUnderline>}
                |    \sout{<styleStrikethrough>Strikethrough</styleStrikethrough>}
                |    \textsc{<styleSmallCapitals>SMALL CAPITALS</styleSmallCapitals>}
                |    \overline{<styleOverline>Overline</styleOverline>}
                |    \texttt{<styleTypewriter>Typewriter</styleTypewriter>}
                |    \textsl{<styleSlanted>Slanted</styleSlanted>}
                |
                |    \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.LatexLexerAdapter
import nl.hannahsten.texifyidea.psi.LatexTypes

/**
 * @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.COMMENT_TOKEN) {
            COMMENT_KEYS
        }
        else if (COMMAND_TOKENS.contains(tokenType)) {
            COMMAND_KEYS
        }
        else if (tokenType == LatexTypes.INLINE_MATH_END) {
            INLINE_MATH_KEYS
        }
        else if (tokenType == LatexTypes.DISPLAY_MATH_START || tokenType == LatexTypes.DISPLAY_MATH_END) {
            DISPLAY_MATH_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 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 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)
        private val COMMENT_KEYS = keys(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.codeInsight.CodeInsightSettings
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 nl.hannahsten.texifyidea.file.LatexFile
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

/**
 * @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) {
                    // 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) {
            if (c == '$' && 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())
                    return Result.STOP
                }
            }
            else if (c == '[') {
                return insertDisplayMathClose(editor)
            }
            else if (c == '(') {
                return insertRobustInlineMathClose(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
            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
    }

    /**
     * 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.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
 */
object BibtexEntryIndex : StringStubIndexExtension<BibtexEntry>() {

    /**
     * Get all the indexed [BibtexEntry]s in the project.
     */
    @JvmStatic
    fun getIndexedEntries(project: Project) = getIndexedEntries(project, GlobalSearchScope.projectScope(project))

    /**
     * Get all the indexed [BibtexEntry]s in the given file.
     */
    @JvmStatic
    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.
     */
    @JvmStatic
    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.
     */
    @JvmStatic
    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
    }

    @JvmStatic
    fun getEntryByName(name: String, project: Project, scope: GlobalSearchScope): Collection<BibtexEntry> {
        return StubIndex.getElements(key, name, project, scope, BibtexEntry::class.java)
    }

    @JvmStatic
    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.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> {
        val result = ArrayList<LatexCommands>()

        for (key in getKeys(project)) {
            val commands = getItemsByName(key, project, scope)
            for (cmd in commands) {
                val token = cmd.name

                if (token in names) {
                    result.add(cmd)
                }
            }
        }

        return result
    }

    /**
     * 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

/**
 * @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 LABELED_ENVIRONMENTS_KEY =
        StubIndexKey.createIndexKey<String, LatexEnvironment>("nl.hannahsten.texifyidea.parameterlabeledenvironments")
}

@file:Suppress("UnusedImport")

package nl.hannahsten.texifyidea.index

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.search.GlobalSearchScope
import com.intellij.psi.stubs.StubIndex
import com.intellij.psi.stubs.StubIndexKey
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>
) {

    /**
     * Get all the items in the index in the given file set.
     * Consider using [PsiFile.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)
        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 [PsiFile.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.
     */
    fun getItems(file: PsiFile) = getItems(file.project, GlobalSearchScope.fileScope(file))

    /**
     * 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): Collection<T> {
        val result = ArrayList<T>()
        for (key in getKeys(project)) {
            result.addAll(getItemsByName(key, project, scope))
        }
        return result
    }

    /**
     * Get all the items in the index that have the given name.
     *
     * @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.
     */
    fun getItemsByName(name: String, project: Project, scope: GlobalSearchScope): Collection<T> {
        return StubIndex.getElements(indexKey, name, project, scope, elementClass)
    }

    /**
     * Get all the keys in the index.
     *
     * @param project
     *          The project instance.
     */
    fun getKeys(project: Project): Array<String> {
        return if (!DumbService.isDumb(project)) {
            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.psi.LatexCommands

/**
 * @author Hannah Schellekens
 */
open class LatexCommandsIndex : StringStubIndexExtension<LatexCommands>() {

    companion object : IndexCommandsUtilBase(IndexKeys.COMMANDS_KEY)

    override fun getKey() = Companion.key()
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
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()
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
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()
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * @author Hannah Schellekens
 */
class LatexIncludesIndex : StringStubIndexExtension<LatexCommands>() {

    companion object : IndexCommandsUtilBase(IndexKeys.INCLUDES_KEY)

    @Suppress("RedundantCompanionReference")
    override fun getKey() = Companion.key()
}

package nl.hannahsten.texifyidea.index

import com.intellij.psi.stubs.StringStubIndexExtension
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()
}

package nl.hannahsten.texifyidea.insight

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.insight

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.LatexRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.firstParentOfType
import nl.hannahsten.texifyidea.util.getRequiredArgumentValueByName
import nl.hannahsten.texifyidea.util.usesColor
import java.awt.Color

object LatexElementColorProvider : ElementColorProvider {
    override fun setColorTo(element: PsiElement, color: Color) {}

    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 Magic.Colors.colorDefinitions.map { it.command } -> {
                        command?.getRequiredArgumentValueByName("name")
                    }
                    // Show the used color.
                    in Magic.Colors.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 = Magic.Colors.defaultXcolors[colorName]

        return if (defaultHex != null) Color(defaultHex)
        else {
            val colorDefiningCommands = LatexCommandsIndex.getCommandsByNames(file, *Magic.Colors.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)) {
                    LatexRegularCommand.COLORLET.command -> {
                        getColorFromColorParameter(file, colorDefinitionCommand.getRequiredArgumentValueByName("color"))
                    }
                    LatexRegularCommand.DEFINECOLOR.command, LatexRegularCommand.PROVIDECOLOR.command -> {
                        getColorFromDefineColor(
                                colorDefinitionCommand.getRequiredArgumentValueByName("model-list"),
                                colorDefinitionCommand.getRequiredArgumentValueByName("spec-list")
                        )
                    }
                    else -> getColorFromColorParameter(file, colorName)
                }
            }
            else return null
        }
    }

    /**
     * Given the [color] parameter 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 null
        }
        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.toLowerCase()) {
                "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())
        }
    }

    private fun fromRgbString(rgbText: String): Color {
        val rgb = rgbText.split(",").map { it.trim() }
        return try {
            rgb.map { it.toInt() }.let { Color(it[0], it[1], it[2]) }
        }
        catch (e: NumberFormatException) {
            rgb.map { it.toFloat() }.let { Color(it[0], it[1], it[2]) }
        }
    }

    private fun fromHsbString(hsbText: String): Color {
        val hsb = hsbText.split(",").map { it.trim() }
        return hsb.map { it.toFloat() }
                .let { Color.getHSBColor(it[0], it[1], it[2]) }
    }

    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() }
                .let { Color(it[0], it[1], it[2]) }
    }

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

    private fun fromGrayString(grayText: String): Color {
        fun Float.toRgb() = (this * 255).toInt()
        val gray = grayText.toFloat()
        return Color(gray.toRgb(), gray.toRgb(), gray.toRgb())
    }

    private fun fromHtmlString(htmlText: String): Color {
        return Color.decode("#$htmlText")
    }
}

package nl.hannahsten.texifyidea.insight

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

/**
 * @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.insight

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

/**
 * 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 Magic.Command.sectionMarkers) {
            LineMarkersPass.createMethodSeparatorLineMarker(element.commandToken, colourManager).apply {
                separatorColor = Magic.Command.sectionSeparatorColors[commandToken]
            }
        }
        else null
    }
}

package nl.hannahsten.texifyidea.insight

import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.lang.parameterInfo.*
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.ArrayUtil
import nl.hannahsten.texifyidea.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * @author Sten Wessel
 */
class LatexParameterInfoHandler : ParameterInfoHandler<LatexCommands, LatexRegularCommand> {

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

    override fun couldShowInLookup() = true

    override fun getParametersForLookup(item: LookupElement, context: ParameterInfoContext): Array<Any>? {
        return ArrayUtil.EMPTY_OBJECT_ARRAY
    }

    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: LatexRegularCommand?, 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.commandDisplay + cmd.getArgumentsDisplay(), 0, 0, false, false, true, context.defaultParameterColor)
    }
}

package nl.hannahsten.texifyidea.inspections

import com.intellij.codeInspection.ex.InspectionToolRegistrar
import nl.hannahsten.texifyidea.insight.InsightGroup

/**
 * 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.grazie.grammar.strategy.GrammarCheckingStrategy
import com.intellij.grazie.grammar.strategy.StrategyUtils
import com.intellij.grazie.grammar.strategy.impl.RuleGroup
import com.intellij.grazie.utils.parents
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.*

class LatexGrammarCheckingStrategy : GrammarCheckingStrategy {
    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

    override fun isMyContextRoot(element: PsiElement) =
        element is LatexNormalText && element.isNotInMathEnvironment() && element.isNotInSquareBrackets() || element is PsiComment

    override fun getStealthyRanges(root: PsiElement, text: CharSequence) = StrategyUtils.indentIndexes(text, setOf(' '))

    override fun isEnabledByDefault() = true

    override fun getContextRootTextDomain(root: PsiElement): GrammarCheckingStrategy.TextDomain {
        return when (root) {
            is PsiComment -> GrammarCheckingStrategy.TextDomain.COMMENTS
            is LatexNormalText -> GrammarCheckingStrategy.TextDomain.PLAIN_TEXT
            is LatexCommands -> GrammarCheckingStrategy.TextDomain.NON_TEXT
            is LatexParameter -> GrammarCheckingStrategy.TextDomain.LITERALS
            else -> GrammarCheckingStrategy.TextDomain.NON_TEXT
        }
    }

    override fun getIgnoredRuleGroup(root: PsiElement, child: PsiElement): RuleGroup? {
        return RuleGroup.PUNCTUATION
    }
}

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.LatexLanguage
import nl.hannahsten.texifyidea.lang.Argument
import nl.hannahsten.texifyidea.lang.Argument.Type
import nl.hannahsten.texifyidea.lang.LatexMathCommand
import nl.hannahsten.texifyidea.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.hasParent

/**
 * @author Hannah Schellekens
 */
class LatexSpellcheckingStrategy : SpellcheckingStrategy() {

    override fun isMyContext(psiElement: PsiElement): Boolean {
        return psiElement.language == LatexLanguage.INSTANCE
    }

    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) {
            return TEXT_TOKENIZER
        }

        if (argument == null || 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
    }

    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.requiredParameters
        val parameterIndex = realParams.indexOf(leaf.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
    }
}

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.util.SmartList
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.insight.InsightGroup
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.isComment
import nl.hannahsten.texifyidea.util.name
import nl.hannahsten.texifyidea.util.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)) }

            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: 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.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: PsiFile) : SuppressionFixBase(file) {

        override val suppressionScope = MagicCommentScope.FILE

        override fun getFamilyName() = "Suppress for file '${file.name}'"
    }

    /**
     * @author Hannah Schellekens
     */
    private inner class EnvironmentSuppressionFix(parentEnvironment: LatexEnvironment) : SuppressionFixBase(parentEnvironment) {

        /**
         * 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) {

        override val suppressionScope = MagicCommentScope.MATH_ENVIRONMENT

        override fun getFamilyName() = "Suppress for math environment"
    }

    /**
     * @author Hannah Schellekens
     */
    private inner class CommandSuppressionFix(parentCommand: LatexCommands) : SuppressionFixBase(parentCommand) {

        /**
         * 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) {

        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.isBlank() }
                .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.insight.InsightGroup
import nl.hannahsten.texifyidea.psi.LatexRawText
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
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, WEAK_WARNING by default.
         */
        val highlight: ProblemHighlightType = ProblemHighlightType.WEAK_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 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,
                    textRange,
                    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)
    }

    override fun runForWholeFile(): Boolean {
        // 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.
        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.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.LatexPsiHelper
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.isLatexFile
import kotlin.math.max

/**
 * @author Hannah Schellekens
 */
open class LatexAddLabelIntention : TexifyIntentionBase("Add label") {

    private fun findCommand(editor: Editor?, file: PsiFile?): LatexCommands? {
        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? LatexCommands ?: element.parentOfType(LatexCommands::class)
        ?: file.findElementAt(max(0, offset - 1)) as? LatexCommands
        ?: file.findElementAt(max(0, offset - 1))?.parentOfType(LatexCommands::class)
        ?: return null
    }

    override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
        if (file?.isLatexFile() == false) {
            return false
        }

        return findCommand(editor, file)?.name in Magic.Command.labeledPrefixes
    }

    override fun startInWriteAction() = true

    override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
        if (editor == null || file == null) {
            return
        }

        val command = findCommand(editor, file) ?: return

        // Determine label name.
        val required = command.requiredParameters
        if (required.isEmpty()) {
            return
        }

        val createdLabel = getUniqueLabelName(required[0].formatAsLabel(),
            Magic.Command.labeledPrefixes[command.name!!], command.containingFile)

        val factory = LatexPsiHelper(project)

        // Insert label
        // command -> NoMathContent -> Content -> Container containing the command
        val commandContent = command.parent.parent
        val labelCommand = commandContent.parent.addAfter(factory.createLabelCommand(createdLabel), commandContent)

        // Adjust caret offset.
        val caret = editor.caretModel
        caret.moveToOffset(labelCommand.endOffset())
    }

    private fun getUniqueLabelName(base: String, prefix: String?, file: PsiFile): String {
        val labelBase = "$prefix:$base"
        val allLabels = file.findLatexAndBibtexLabelStringsInFileSet()
        return appendCounter(labelBase, allLabels)
    }

    /**
     * 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.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.endCommand
import nl.hannahsten.texifyidea.util.environmentName
import nl.hannahsten.texifyidea.util.files.isLatexFile
import nl.hannahsten.texifyidea.util.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

/**
 * @author Hannah Schellekens
 */
open class LatexInlineDisplayToggle : 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.*

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 Magic.Environment.verbatim.any { it == 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.lang.Language
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.LatexLanguage
import nl.hannahsten.texifyidea.file.LatexFile
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.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
        }

        val element = file.findElementAt(editor.caretModel.offset) ?: return

        chooseLanguage(editor) { language ->
            val comment = MutableMagicComment<String, String>().apply { addValue(DefaultMagicKeys.INJECT_LANGUAGE, language.id) }
            WriteCommandAction.runWriteCommandAction(project) {
                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.INSTANCE.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<Injectable>(list).setItemChoosenCallback { onChosen(list.selectedValue) }
                .setFilteringEnabled { language -> (language as Injectable).displayName }
                .setMinSize(dimension)
                .createPopup()

        popup.showInBestPositionFor(editor)
    }

    private fun injectableLanguages(): List<Injectable> = Language.getRegisteredLanguages().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.*
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.isLatexFile

/**
 * @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 && nested <= 0) {
                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 && nested <= 0) {
                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.*
import nl.hannahsten.texifyidea.util.files.isLatexFile

/**
 * @author Hannah Schellekens, Abby Berkers
 */
open class LatexMathToggle : 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.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.LatexTooLargeSectionInspection.Companion.findNextSection
import nl.hannahsten.texifyidea.inspections.latex.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.createFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.isLatexFile
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 ?: return) - 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 = CreateFileDialog(file.containingDirectory?.virtualFile?.canonicalPath, fileName.formatAsFileName())
                .newFileFullPath ?: return

        // Execute write actions.
        runWriteAction {
            val createdFile = createFile("$filePath.tex", text)
            document.deleteString(start, end)
            val fileNameRelativeToRoot = createdFile.absolutePath
                    .replace(File.separator, "/")
                    .replace("$root/", "")
            val indent = sectionCommand.findIndentation()
            document.insertString(start, "\n$indent\\input{${fileNameRelativeToRoot.dropLast(4)}}\n\n")
        }
    }
}

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.createFile
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.isLatexFile
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).sumBy { (start, end) -> 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.
        @Language("RegExp")
        // Note that we do not override the user-specified filename to be LaTeX-like.
        // Path of virtual file always contains '/' as file separators.
        val root = file.findRootFile().containingDirectory?.virtualFile?.canonicalPath

        // Execute write actions.
        runWriteAction {
            val createdFile = createFile("$filePath.tex", text.toString())

            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 settings = TexifySettings.getInstance()

        private val triggerCharacters = setOf('\'', '"', '`')
    }

    /**
     * Contains the preferred setting of the user if they have already selected one.
     */
    private var selectedSmartQuoteSetting = when (val replacement = settings.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 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

/**
 * @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 = Magic.Pattern.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.codeInsight.intention.IntentionAction
import nl.hannahsten.texifyidea.insight.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

/**
 * @author Sten Wessel
 */
abstract class Argument @JvmOverloads protected constructor(val name: String, val type: Type = Type.NORMAL) {

    abstract override fun toString(): String

    enum class Type {
        NORMAL,
        FILE,
        TEXT
    }
}

package nl.hannahsten.texifyidea.lang

import nl.hannahsten.texifyidea.lang.BibtexDefaultEntryType.*
import nl.hannahsten.texifyidea.lang.Package.Companion.BIBLATEX

/**
 * @author Hannah Schellekens
 */
enum class BibtexDefaultEntry(
        override val token: String,
        override val description: String,
        override val required: Array<BibtexEntryField>,
        override val optional: Array<BibtexEntryField>,
        override val dependency: Package = Package.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
    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
    ),
    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
    ),
    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
    ),
    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
    ),
    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
    ),
    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
    ),
    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
    );

    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.toLowerCase()
            if (token.startsWith("@")) {
                trimmedToken = token.substring(1)
            }
            return lookup[trimmedToken]
        }
    }
}

package nl.hannahsten.texifyidea.lang

import nl.hannahsten.texifyidea.lang.Package.Companion.BIBLATEX
import nl.hannahsten.texifyidea.lang.Package.Companion.DEFAULT

/**
 * @author Hannah Schellekens
 */
enum class BibtexDefaultEntryType(
        override val fieldName: String,
        override val description: String,
        override val dependency: Package = 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 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) {
        Package.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 com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.*
import java.io.Serializable
import java.util.Collections
import java.util.HashSet
import java.util.Spliterator
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Stream
import java.util.stream.StreamSupport

/**
 * 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 {
    /**
     * Maps a command to a set of aliases including the command itself.
     *
     *
     * 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`
     */
    private val aliases = ConcurrentHashMap<String, MutableSet<String>>()

    /**
     * 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>>()

    /**
     * We have to somehow know when we need to look for new aliases.
     * We do this by keeping a count of the number of \newcommand-like commands in the index,
     * and when this changes we go gather new aliases.
     */
    private var numberOfIndexedCommandDefinitions = 0

    /**
     * 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> = Magic.Command.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) {
        require(!isRegistered(command)) { "command '$command' has already been registered" }
        val aliasSet: MutableSet<String> = HashSet()
        aliasSet.add(command)
        original[command] = aliasSet
        synchronized(aliases) {
            aliases[command] = aliasSet
        }
    }

    /**
     * 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 command
     * An existing command to register the alias for starting with a backslash. *E.g.
     * `\begin`*
     * @param alias
     * The alias to register for the command starting with a backslash. This could be either
     * a new command, or an existing command *E.g. `\start`*
     */
    @Synchronized
    fun registerAlias(command: String, alias: String) {
        synchronized(aliases) {
            val aliasSet = aliases[command] ?: 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
        }
    }

    /**
     * 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)
    }

    /**
     * Get an unmodifiable set with all the aliases for the given command.
     *
     * @param command
     * An existing command to get all aliases of starting with a backslash. *E.g. `\begin`*
     * @return An unmodifiable set of all aliases including the command itself. All aliases include
     * a command backslash. Returns the empty set if the command is not registered.
     */
    fun getAliases(command: String): Set<String> {
        if (!isRegistered(command)) return emptySet()
        synchronized(aliases) {
            return aliases[command]?.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: Set<String>, project: Project) {
        // Register if needed
        if (aliasSet.isEmpty()) return
        val firstAlias = aliasSet.first()
        var wasRegistered = false
        if (!isRegistered(firstAlias)) {
            registerCommand(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)

        // Also do this the first time something is registered, because then we have to update aliases as well
        if (numberOfIndexedCommandDefinitions != indexedCommandDefinitions.size || 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)
                }
            }

            numberOfIndexedCommandDefinitions = indexedCommandDefinitions.size
        }
    }

    private fun findAllAliases(
        aliasSet: Set<String>,
        indexedCommandDefinitions: 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)
        indexedCommandDefinitions.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(Magic.Command.labelDefinitionsWithoutCustomCommands).isNotEmpty()) {
            indexedCommandDefinitions.forEach { commandDefinition ->
                val definedCommand = commandDefinition.requiredParameter(0) ?: return@forEach
                if (definedCommand.isBlank()) return@forEach

                val isFirstParameterOptional = commandDefinition.optionalParameters.size > 1

                val parameterCommands = commandDefinition.requiredParameters().getOrNull(1)
                    ?.requiredParamContentList
                    ?.flatMap { it.childrenOfType(LatexCommands::class) }
                    ?.asSequence()
                    ?.filterNotNull()

                // Positions of label parameters in the custom commands (starting from 0)
                val positions = parameterCommands
                    ?.filter { it.name in Magic.Command.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 Magic.Command.labelDefinitionsWithoutCustomCommands }
                        .any { it.name in Magic.Command.increasesCounter }

                val prefix = parameterCommands.filter { it.name in Magic.Command.labelDefinitionsWithoutCustomCommands }
                    .mapNotNull { it.requiredParameter(0) }
                    .map {
                        if (it.indexOf('#') != -1) {
                            val prefix = it.substring(0, it.indexOf('#'))
                            if (prefix.isNotBlank()) prefix else ""
                        }
                        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 command
     * The command to check if it has been registered starting with the command backslash.
     * *E.g. `\begin`*
     * @return `true` if the given command is present in the command manager, `false`
     * otherwise.
     */
    fun isRegistered(command: String): Boolean {
        synchronized(aliases) {
            return aliases.containsKey(command)
        }
    }

    /**
     * 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

import nl.hannahsten.texifyidea.lang.Environment.Context
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.Package.Companion.GAUSS
import nl.hannahsten.texifyidea.lang.Package.Companion.MATHTOOLS
import nl.hannahsten.texifyidea.lang.Package.Companion.XCOLOR
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.name
import java.util.HashMap

/**
 * @author Hannah Schellekens, Sten Wessel
 */
enum class DefaultEnvironment(
        override vararg val arguments: Argument,
        override val environmentName: String,
        override val initialContents: String = "",
        override val context: Context = Context.NORMAL,
        override val dependency: Package = Package.DEFAULT
) : Environment {

    // Vanilla LaTeX
    ABSTRACT(environmentName = "abstract"),
    ALLTT(environmentName = "alltt"),
    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"),
    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(RequiredArgument("label"), RequiredArgument("spacing"), environmentName = "list"),
    LRBOX(environmentName = "lrbox"),
    MATH(environmentName = "math"),
    MINIPAGE(OptionalArgument("position"), RequiredArgument("width"), environmentName = "minipage"),
    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"))),
    TABULAR(OptionalArgument("pos"), RequiredArgument("cols"), environmentName = "tabular"),
    TABULAR_STAR(RequiredArgument("width"), OptionalArgument("pos"), RequiredArgument("cols"), environmentName = "tabular*"),
    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_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),

    // gauss
    GMATRIX(environmentName = "gmatrix", context = Context.MATH, dependency = GAUSS),

    // comment
    COMMENT(environmentName = "comment", context = Context.COMMENT, dependency = Package.COMMENT),

    // lualatex
    LUACODE(environmentName = "luacode", dependency = Package.LUACODE),

    // listings
    LISTINGS(environmentName = "lstlisting", dependency = Package.LISTINGS),

    // tikz
    TIKZPICTURE(environmentName = "tikzpicture", dependency = Package.TIKZ),

    // xcolor
    TESTCOLORS(environmentName = "testcolors", arguments = *arrayOf(OptionalArgument("num models")), dependency = XCOLOR);

    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.toLowerCase())
        }

        /**
         * 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 [Package.DEFAULT] when no package is needed.
     */
    val dependency: Package
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Sten Wessel
 */
interface Diacritic {

    companion object {

        fun buildChain(base: String, diacritics: List<Diacritic?>): String? {
            if (diacritics.contains(null)) {
                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

/**
 * @author Hannah Schellekens
 */
interface Environment : Dependend {

    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]

        /**
         * @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

import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.inMathContext
import kotlin.reflect.KClass

/**
 * @author Hannah Schellekens, Sten Wessel
 */
interface LatexCommand : Dependend {

    companion object {

        /**
         * Looks up the given command name in all [LatexMathCommand]s and [LatexRegularCommand]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?): Set<LatexCommand>? {
            var result = commandName ?: return null
            if (result.startsWith("\\")) {
                result = result.substring(1)
            }

            return LatexMathCommand[result] ?: LatexRegularCommand[result]
        }

        /**
         * 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 name = command.commandToken.text
            val commandName = name.substring(1)

            return if (command.inMathContext()) {
                LatexMathCommand[commandName]
            }
            else LatexRegularCommand[commandName]
        }
    }

    /**
     * Get the name of the command without the first backslash.
     */
    val command: String

    val commandDisplay: String
        get() = "\\$command"

    /**
     * Get the display value of the command.
     */
    val display: String?

    /**
     * Get all the command arguments.
     */
    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()
    }

    /**
     * Checks whether `{}` must be automatically inserted in the auto complete.
     *
     * @return `true` to insert automatically, `false` not to insert.
     */
    fun autoInsertRequired() = arguments.filterIsInstance<RequiredArgument>().count() >= 1

    @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

import nl.hannahsten.texifyidea.lang.Argument.Type
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSFONTS
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSSYMB
import nl.hannahsten.texifyidea.lang.Package.Companion.DEFAULT
import nl.hannahsten.texifyidea.lang.Package.Companion.LATEXSYMB

/**
 * @author Sten Wessel
 */
@Suppress("unused")
enum class LatexMathCommand(
        override val command: String,
        override vararg val arguments: Argument = emptyArray(),
        override val dependency: Package = DEFAULT,
        override val display: String? = null,
        val collapse: Boolean = false
) : LatexCommand {

    /*
     *  Greek alphabet
     */
    ALPHA("alpha", display = "α", collapse = true),
    BETA("beta", display = "β", collapse = true),
    GAMMA("gamma", display = "γ", collapse = true),
    CAPITAL_GAMMA("Gamma", display = "Γ", collapse = true),
    DELTA("delta", display = "δ", collapse = true),
    CAPITAL_DELTA("Delta", display = "Δ", collapse = true),
    EPSILON("epsilon", display = "ϵ", collapse = true),
    EPSILON_SYMBOL("varepsilon", display = "ε", collapse = true),
    ZETA("zeta", display = "ζ", collapse = true),
    ETA("eta", display = "η", collapse = true),
    THETA("theta", display = "θ", collapse = true),
    THETA_SYMBOL("vartheta", display = "ϑ", collapse = true),
    CAPITAL_THETA("Theta", display = "Θ", collapse = true),
    IOTA("iota", display = "ι", collapse = true),
    KAPPA("kappa", display = "κ", collapse = true),
    LAMBDA("lambda", display = "λ", collapse = true),
    CAPITAL_LAMBDA("Lambda", display = "Λ", collapse = true),
    MU("mu", display = "μ", collapse = true),
    NU("nu", display = "ν", collapse = true),
    XI("xi", display = "ξ", collapse = true),
    CAPITAL_XI("Xi", display = "Ξ", collapse = true),
    PI("pi", display = "π", collapse = true),
    PI_SYMBOL("varpi", display = "ϖ", collapse = true),
    CAPITAL_PI("Pi", display = "Π", collapse = true),
    RHO("rho", display = "ρ", collapse = true),
    RHO_SYMBOL("varrho", display = "ϱ", collapse = true),
    SIGMA("sigma", display = "σ", collapse = true),
    FINAL_SIGMA("varsigma", display = "ς", collapse = true),
    CAPTIAL_SIGMA("Sigma", display = "Σ", collapse = true),
    TAU("tau", display = "τ", collapse = true),
    UPSILON("upsilon", display = "υ", collapse = true),
    PHI("phi", display = "φ", collapse = true),
    PHI_SYMBOL("varphi", display = "ϕ", collapse = true),
    CAPITAL_PHI("Phi", display = "Φ", collapse = true),
    CHI("chi", display = "χ", collapse = true),
    PSI("psi", display = "ψ", collapse = true),
    CAPITAL_PSI("Psi", display = "Ψ", collapse = true),
    OMEGA("omega", display = "ω", collapse = true),
    CAPITAL_OMEGA("Omega", display = "Ω", collapse = true),

    /*
     *  Operators
     */
    FORALL("forall", display = "∀", collapse = true),
    PARTIAL("partial", display = "∂", collapse = true),
    EXISTS("exists", display = "∃", collapse = true),
    NEXISTS("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 = "∁"),
    BIG_PRODUCT("prod", display = "∏", collapse = true),
    COPRODUCT("coprod", display = "∐", collapse = true),
    SUM("sum", display = "∑", collapse = true),
    MINUS_PLUS("mp", display = "∓", collapse = true),
    SET_MINUS("setminus", display = "∖", collapse = true),
    SMALL_SET_MINUS("smallsetminus", dependency = AMSSYMB, display = "∖", collapse = true),
    ASTERISK("ast", display = "∗"),
    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),
    PARALLEL("parallel", display = "∥", collapse = true),
    NOT_PARALLEL("nparallel", 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),
    INTEGRAL("int", display = "∫", collapse = true),
    DOUBLE_INTEGRAL("iint", dependency = AMSMATH, display = "∬", collapse = true),
    TRIPLE_INTEGRAL("iiint", dependency = AMSMATH, display = "∭", collapse = true),
    QUADRUPLE_INTEGRAL("iiiint", dependency = AMSMATH, 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_EQUALL("leqq", dependency = AMSSYMB, display = "≦", collapse = true),
    GREATER_THAN_EQUAL("geq", 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"),
    LESSER_LESSER("ll", dependency = AMSSYMB, display = "≪", collapse = true),
    LESSER_LESSER_LESSER("lll", dependency = AMSSYMB, display = "⋘", collapse = true),
    LESSER_NOT_EQUAL("lneqq", dependency = AMSSYMB, display = "≨", collapse = true),
    GREATER_NOT_EQUAL("gneqq", dependency = AMSSYMB, display = "≩", collapse = true),
    GREATER_GREATER("gg", dependency = AMSSYMB, display = "≫", collapse = true),
    GREATER_GREATER_GREATER("ggg", dependency = AMSSYMB, display = "⋙", collapse = true),
    SUBSET("subset", display = "⊂", collapse = true),
    SUPSET("supset", display = "⊃", collapse = true),
    SUBSET_EQ("subseteq", display = "⊆", collapse = true),
    SUBSET_EQQ("subseteqq", dependency = AMSSYMB, display = "⊆", collapse = true),
    SUPSET_EQ("supseteq", display = "⊇", collapse = true),
    SUPSET_EQQ("supseteqq", dependency = AMSSYMB, display = "⊇", collapse = true),
    NOT_SUBSET_EQ("nsubseteq", dependency = AMSSYMB, display = "⊈", collapse = true),
    NOT_SUBSET_EQQ("nsubseteqq", dependency = AMSSYMB, display = "⊈", collapse = true),
    NOT_SUPSET_EQ("nsupseteq", dependency = AMSSYMB, display = "⊉", collapse = true),
    NOT_SUPSET_EQQ("nsupseteqq", dependency = AMSSYMB, display = "⊉", collapse = true),
    SQUARE_SUBSET("sqsubset", dependency = AMSSYMB, display = "⊏", collapse = true),
    SQUARE_SUPSET("sqsupset", dependency = AMSSYMB, display = "⊐", collapse = true),
    SQUARE_SUBSET_EQ("sqsubseteq", dependency = AMSSYMB, display = "⊑", collapse = true),
    SQUARE_SUPSET_EQ("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),
    TRIANGLERIGHT("triangleright", dependency = AMSSYMB, display = "▷", collapse = true),
    TRIANGLELEFT("triangleleft", dependency = AMSSYMB, display = "◁", collapse = true),
    VARTRIANGLERIGHT("vartriangleright", dependency = AMSSYMB, display = "⊳", collapse = true),
    VARTRIANGLELEFT("vartriangleleft", 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/Right
     */
    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 = "||"),
    LANGLE("langle", display = "<"),
    RANGLE("rangle", display = ">"),

    /*
     *  Arrows
     */
    NRIGHTARROW("nrightarrow", dependency = AMSSYMB, display = "↛", collapse = true),
    LEFTARROW("leftarrow", display = "←", collapse = true),
    L_EFTARROW("Leftarrow", display = "⇐", collapse = true),
    RIGHTARROW("rightarrow", display = "→", collapse = true),
    R_IGHTARROW("Rightarrow", display = "⇒", collapse = true),
    LEFTRIGHTARROW("leftrightarrow", display = "↔", collapse = true),
    L_EFTRIGHTARROW("Leftrightarrow", display = "⇔", collapse = true),
    LONGLEFTARROW("longleftarrow", display = "⟵", collapse = true),
    L_ONGLEFTARROW("Longleftarrow", display = "⟸", collapse = true),
    LONGRIGHTARROW("longrightarrow", display = "⟶", collapse = true),
    L_ONGRIGHTARROW("Longrightarrow", display = "⟹", collapse = true),
    LONGLEFTRIGHTARROW("longleftrightarrow", display = "⟷", collapse = true),
    L_ONGLEFTRIGHTARROW("Longleftrightarrow", display = "⟺", collapse = true),
    UPARROW("uparrow", display = "↑", collapse = true),
    U_PARROW("Uparrow", display = "⇑", collapse = true),
    DOWNARROW("downarrow", display = "↓", collapse = true),
    D_OWNARROW("Downarrow", display = "⇓", collapse = true),
    UPDOWNARROW("updownarrow", display = "↕", collapse = true),
    U_PDOWNARROW("Updownarrow", display = "⇕", collapse = true),
    MAPSTO("mapsto", display = "↦", collapse = true),
    HOOKLEFTARROW("hookleftarrow", display = "↩", collapse = true),
    LEFTHARPOONUP("leftharpoonup", display = "↼", collapse = true),
    LEFTHARPOONDOWN("leftharpoondown", display = "↽", collapse = true),
    RIGHTLEFTHARPOONS("rightleftharpoons", display = "⇌", collapse = true),
    LONGMAPSTO("longmapsto", display = "⟼", collapse = true),
    HOOKRIGHTARROW("hookrightarrow", display = "↪", collapse = true),
    RIGHTHARPOONUP("rightharpoonup", display = "⇀", collapse = true),
    RIGHTHARPOONDOWN("rightharpoondown", display = "⇁", collapse = true),
    LEADSTO("leadsto", dependency = LATEXSYMB, display = "⤳", collapse = true),
    NEARROW("nearrow", display = "↗", collapse = true),
    SEARROW("searrow", display = "↘", collapse = true),
    SWARROW("swarrow", display = "↙", collapse = true),
    NWARROW("nwarrow", display = "↖", collapse = true),
    DASHRIGHTARROW("dashrightarrow", dependency = AMSSYMB, display = "⤍", collapse = true),
    LEFTRIGHTARROWS("leftrightarrows", dependency = AMSSYMB, display = "⇆", collapse = true),
    LEFTARROWTAIL("leftarrowtail", dependency = AMSSYMB, display = "↢", collapse = true),
    CURVEARROWLEFT("curvearrowleft", dependency = AMSSYMB, display = "↶", collapse = true),
    UPUPARROWS("upuparrows", dependency = AMSSYMB, display = "⇈", collapse = true),
    MULTIMAP("multimap", dependency = AMSSYMB, display = "⊸", collapse = true),
    RIGHTLEFTARROWS("rightleftarrows", dependency = AMSSYMB, display = "⇄", collapse = true),
    TWOHEADRIGHTARROW("twoheadrightarrow", dependency = AMSSYMB, display = "↠", collapse = true),
    RSH("Rsh", dependency = AMSSYMB, display = "↱", collapse = true),
    DOWNHARPOONRIGHT("downharpoonright", dependency = AMSSYMB, display = "⇂", collapse = true),
    DASHLEFTARROW("dashleftarrow", dependency = AMSSYMB, display = "⇠", collapse = true),
    L_LEFTARROW("Lleftarrow", dependency = AMSSYMB, display = "⇚", collapse = true),
    LOOPARROWLEFT("looparrowleft", dependency = AMSSYMB, display = "↫", collapse = true),
    CIRCLEARROWLEFT("circlearrowleft", dependency = AMSSYMB, display = "↺", collapse = true),
    UPHARPOONLEFT("upharpoonleft", dependency = AMSSYMB, display = "↿", collapse = true),
    LEFTRIGHTSQUIGARROW("leftrightsquigarrow", dependency = AMSSYMB, display = "↭", collapse = true),
    RIGHTRIGHTARROWS("rightrightarrows", dependency = AMSSYMB, display = "⇉", collapse = true),
    RIGHTARROWTAIL("rightarrowtail", dependency = AMSSYMB, display = "↣", collapse = true),
    CURVEARROWRIGHT("curvearrowright", dependency = AMSSYMB, display = "↷", collapse = true),
    DOWNDOWNARROWS("downdownarrows", dependency = AMSSYMB, display = "⇊", collapse = true),
    RIGHTSQUIGARROW("rightsquigarrow", dependency = AMSSYMB, display = "⇝", collapse = true),
    LEFTLEFTARROWS("leftleftarrows", dependency = AMSSYMB, display = "⇇", collapse = true),
    TWOHEADLEFTARROW("twoheadleftarrow", dependency = AMSSYMB, display = "↞", collapse = true),
    LEFTRIGHTHARPOONS("leftrightharpoons", dependency = AMSSYMB, display = "↰", collapse = true),
    LSH("Lsh", dependency = AMSSYMB, display = "↿", collapse = true),
    DOWNHARPOONLEFT("downharpoonleft", dependency = AMSSYMB, display = "⇃", collapse = true),
    LOOPARROWRIGHT("looparrowright", dependency = AMSSYMB, display = "↬", collapse = true),
    CIRCLEARROWRIGHT("circlearrowright", dependency = AMSSYMB, display = "↻", collapse = true),
    UPHARPOONRIGHT("rightsquigarrow", dependency = AMSSYMB, display = "↾", collapse = true),
    NLEFTARROW("nleftarrow", dependency = AMSSYMB, display = "↚", collapse = true),
    NL_EFTARROW("nLeftarrow", dependency = AMSSYMB, display = "⇍", collapse = true),
    NR_IGHTARROW("nRightarrow", dependency = AMSSYMB, display = "⇏", collapse = true),
    NLEFTRIGHTARROW("nleftrightarrow", dependency = AMSSYMB, display = "↮", collapse = true),
    NL_EFTRIGHTARROW("nLeftrightarrow", dependency = AMSSYMB, display = "⇎", collapse = true),

    /*
     *  Generic commands
     */
    MATHBB("mathbb", "text".asRequired(Type.TEXT), dependency = AMSFONTS),
    MATHBF("mathbf", "text".asRequired()),
    BM("bm", "text".asRequired(Type.TEXT), dependency = Package.BM),
    MATHCAL("mathcal", "text".asRequired()),
    MATHDS("mathds", "mathds".asRequired()),
    MATHELLIPSIS("mathellipsis"),
    MATHFRAK("mathfrak", "text".asRequired(Type.TEXT), dependency = AMSFONTS),
    MATHGROUP("mathgroup"),
    MATHIT("mathit", "text".asRequired()),
    MATHNORMAL("mathnormal", "text".asRequired()),
    MATHRM("mathrm", "text".asRequired()),
    MATHSCR("mathscr"),
    MATHSF("mathsf", "text".asRequired()),
    MATHSTERLING("mathsterling"),
    MATHTT("mathtt", "text".asRequired()),
    MATHUNDERSCORE("mathunderscore"),
    SQRT("sqrt", "root".asOptional(), "arg".asRequired()),
    ACUTE("acute", "a".asRequired()),
    ALEPH("aleph"),
    AMALG("amalg"),
    ARCCOS("arccos"),
    ARCSIN("arcsin"),
    ARCTAN("arctan"),
    ARG("arg"),
    ARROWVERT("arrowvert", display = "|", collapse = true),
    CAPITAL_ARROWVERT("Arrowvert"),
    ASYMP("asymp", display = "≍", collapse = true),
    BACKSLASH("backslash", display = "\\", collapse = true),
    BAR("bar", "a".asRequired()),
    BIGCAP("bigcap", display = "⋂", collapse = true),
    BIGCIRC("bigcirc", display = "○", collapse = true),
    BIGCUP("bigcup", display = "⋃", collapse = true),
    BIGODOT("bigodot", display = "⨀", collapse = true),
    BIGOPLUS("bigoplus", display = "⨁", collapse = true),
    BIGOTIMES("bigotimes", display = "⨂", collapse = true),
    BIGSQCUP("bigsqcup", display = "⨆", collapse = true),
    BIGTRIANGLEDOWN("bigtriangledown", display = "▽", collapse = true),
    BIGTRIANGLEUP("bigtriangleup", display = "∆", collapse = true),
    BIGUPLUS("biguplus", display = "⨄", collapse = true),
    BIGVEE("bigvee", display = "⋁", collapse = true),
    BIGWEDGE("bigwedge", display = "⋀", collapse = true),
    BINOM("binom", "total".asRequired(), "sample".asRequired(), dependency = AMSMATH),
    BOT("bot", display = "⟂", collapse = true),
    BRACEVERT("bracevert"),
    BREVE("breve", "a".asRequired()),
    CDOT("cdot", display = "·", collapse = true),
    CDOTS("cdots", display = "⋯", collapse = true),
    CHECK("check", "a".asRequired()),
    CLUBSUIT("clubsuit", display = "♣", collapse = true),
    COLON("colon", display = ":", collapse = true),
    CONG("cong"),
    COS("cos"),
    COSH("cosh"),
    COT("cot"),
    COTH("coth"),
    CSC("csc"),
    DAGGER("dagger", display = "†", collapse = true),
    DASHV("dashv", display = "⊣", collapse = true),
    DDAGGER("ddagger", display = "‡", collapse = true),
    DEG("deg"),
    DET("det"),
    DFRAC("dfrac", "num".asRequired(), "den".asRequired(), dependency = AMSMATH),
    DIAMOND("diamond", display = "◇", collapse = true),
    DIAMONDSUIT("diamondsuit", display = "♢", collapse = true),
    DIM("dim"),
    DIV("div", display = "÷", collapse = true),
    DIVIDEONTIMES("divideontimes", dependency = AMSSYMB, display = "⋇", collapse = true),
    DOTEQ("doteq"),
    DOT("dot", "a".asRequired()),
    DOTS("dots", display = "⋯", collapse = true),
    ELL("ell"),
    EXP("exp"),
    FLAT("flat", display = "♭", collapse = true),
    SHARP("sharp", display = "♯", collapse = true),
    FRAC("frac", "num".asRequired(), "den".asRequired()),
    GRAVE("grave", "a".asRequired()),
    HAT("hat", "a".asRequired()),
    MATHRING("mathring", "a".asRequired()),
    OVERBRACE("overbrace", "text".asRequired()),
    OVERLINE("overline", "text".asRequired()),
    TEXT("text", "text".asRequired(), dependency = AMSMATH),
    TILDE("tilde", "a".asRequired()),
    UNDERBRACE("underbrace", "text".asRequired()),
    UNDERLINE("underline", "text".asRequired()),
    VEC("vec", "a".asRequired()),
    VEE("vee", display = "⋁", collapse = true),
    WEDGE("wedge", display = "⋀", collapse = true),
    WIDEHAT("widehat", "text".asRequired()),
    WIDETILDE("widetilde", "text".asRequired());

    companion object {

        private val lookup = HashMap<String, MutableSet<LatexMathCommand>>()
        private val lookupDisplay = HashMap<String, MutableSet<LatexMathCommand>>()

        init {
            for (command in values()) {
                lookup.getOrPut(command.command) { mutableSetOf() }.add(command)
                if (command.display != null) {
                    lookupDisplay.putIfAbsent(command.display!!, mutableSetOf(command))?.add(command)
                }
            }
        }

        @JvmStatic
        operator fun get(command: String) = lookup[command]?.toSet()

        @JvmStatic
        fun findByDisplay(display: String) = lookupDisplay[display]?.toSet()
    }
}

package nl.hannahsten.texifyidea.lang

import java.util.*

/**
 * @author Sten Wessel
 */
enum class LatexMathEnvironment(
        val environmentName: String,
        vararg val arguments: Argument,
        val initialContents: String? = null
) {

    /*
     *  Default LaTeX.
     */
    ARRAY("array", OptionalArgument("pos"), RequiredArgument("cols")),
    BMATRIX("bmatrix"),
    CAPITAL_BMATRIX("Bmatrix"),
    MATRIX("matrix"),
    PMATRIX("pmatrix"),
    VMATRIX("vmatrix"),
    CAPITAL_VMATRIX("Vmatrix");

    companion object {

        private val lookup = HashMap<String, LatexMathEnvironment>()

        init {
            for (environment in values()) {
                lookup[environment.environmentName] = environment
            }
        }

        @JvmStatic
        operator fun get(name: String) = lookup[name]
    }
}

package nl.hannahsten.texifyidea.lang

import nl.hannahsten.texifyidea.lang.Argument.Type
import nl.hannahsten.texifyidea.lang.Package.Companion.ALGPSEUDOCODE
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.Package.Companion.BIBLATEX
import nl.hannahsten.texifyidea.lang.Package.Companion.CLEVEREF
import nl.hannahsten.texifyidea.lang.Package.Companion.CSQUOTES
import nl.hannahsten.texifyidea.lang.Package.Companion.DEFAULT
import nl.hannahsten.texifyidea.lang.Package.Companion.FONTENC
import nl.hannahsten.texifyidea.lang.Package.Companion.GLOSSARIES
import nl.hannahsten.texifyidea.lang.Package.Companion.GRAPHICX
import nl.hannahsten.texifyidea.lang.Package.Companion.MATHTOOLS
import nl.hannahsten.texifyidea.lang.Package.Companion.NATBIB
import nl.hannahsten.texifyidea.lang.Package.Companion.SIUNITX
import nl.hannahsten.texifyidea.lang.Package.Companion.SUBFILES
import nl.hannahsten.texifyidea.lang.Package.Companion.ULEM
import nl.hannahsten.texifyidea.lang.Package.Companion.XCOLOR

/**
 * @author Sten Wessel
 */
enum class LatexRegularCommand(
        override val command: String,
        override vararg val arguments: Argument = emptyArray(),
        override val dependency: Package = DEFAULT,
        override val display: String? = null
) : 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(Type.TEXT)),
    AUTOREF("autoref", "label".asRequired(Type.TEXT), dependency = Package.HYPERREF),
    BEGIN("begin", "environment".asRequired()),
    END("end", "environment".asRequired()),
    ENSUREMATH("ensuremath", "text".asRequired()),
    BASELINESKIP("baselineskip"),
    BASELINESTRETCH("baselinestretch "),
    BFSERIES("bfseries"),
    BIBITEM("bibitem", "label".asOptional(), "citekey".asRequired()),
    BIBLIOGRAPHYSTYLE("bibliographystyle", "style".asRequired()),
    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(Type.TEXT)),
    CHAPTER("chapter", "shorttitle".asOptional(), "title".asRequired(Type.TEXT)),
    CHAPTER_STAR("chapter*", "title".asRequired(Type.TEXT)),
    CITE("cite", "extratext".asOptional(), "keys".asRequired()),
    CLEARDOUBLEPAGE("cleardoublepage"),
    CLEARPAGE("clearpage"),
    COLOR("color", "color".asRequired(), dependency = XCOLOR),
    COLOR2("color", "model-list".asOptional(), "spec-list".asRequired(), dependency = XCOLOR),
    COLORBOX("colorbox", "color".asRequired(), "text".asRequired(), dependency = XCOLOR),
    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(Type.TEXT), "page".asRequired()),
    CONTENTSNAME("contentsname", "name".asRequired()),
    CONVERTCOLORSPEC("convertcolorspec", "model".asRequired(), "spec".asRequired(), "target model".asRequired(), "cmd".asRequired(), dependency = XCOLOR),
    CREF("cref", "label".asRequired(), dependency = CLEVEREF),
    CREF_CAPITAL("Cref", "label".asRequired(), dependency = CLEVEREF),
    DATE("date", "text".asRequired(Type.TEXT)),
    DECLARE_MATH_OPERATOR("DeclareMathOperator", "command".asRequired(), "operator".asRequired(Type.TEXT), dependency = AMSMATH),
    DEF("def"),
    DIRECTLUA("directlua", "luacode".asRequired()),
    DOCUMENTCLASS("documentclass", "options".asOptional(), RequiredFileArgument("class", true, false, "cls")),
    DOTFILL("dotfill"),
    EM("em"),
    EMPH("emph", "text".asRequired(Type.TEXT)),
    ENLARGETHISPAGE("enlargethispage", "size".asRequired()),
    ENLARGETHISPAGE_STAR("enlargethispage*", "size".asRequired()),
    ENQUOTE("enquote", dependency = CSQUOTES),
    ENQUOTE_STAR("enquote*", dependency = CSQUOTES),
    EQREF("eqref", "eqLabel".asRequired(Type.TEXT), dependency = AMSMATH),
    EVENSIDEMARGIN("evensidemargin"),
    EXTERNALDOCUMENT("externaldocument", "prefix".asOptional(), "file".asRequired()),
    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(Type.TEXT)),
    FCOLORBOX("fcolorbox", "frame color".asRequired(), "background color".asRequired(), "text".asRequired(), dependency = XCOLOR),
    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(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(Type.TEXT)),
    FOOTNOTE("footnote", "number".asOptional(), "text".asRequired(Type.TEXT)),
    FOOTNOTEMARK("footnotemark"),
    FRAMEBOX("framebox", "width".asOptional(), "pos".asOptional(), "text".asOptional(Type.TEXT)),
    FRAME("frame", "text".asRequired(Type.TEXT)),
    FRQ("frq", display = "›"),
    FRQQ("frqq", display = "»"),
    FULLREF("fullref", "label".asRequired(Type.TEXT), dependency = Package.HYPERREF),
    GLOSSARYENTRY("glossaryentry", "text".asRequired(Type.TEXT), "pagenum".asRequired()),
    GLOSSARY("glossary", "text".asRequired(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 = Package.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(Type.TEXT), dependency = Package.HYPERREF),
    HYPHENATION("hyphenation", "words".asRequired(Type.TEXT)),
    I("i", display = "i (dotless)"),
    IMPORT("import", RequiredFolderArgument("absolute path"), RequiredFileArgument("filename", false, false, "tex"), dependency = Package.IMPORT),
    INCLUDE("include", RequiredFileArgument("sourcefile", false, false, "tex")),
    INCLUDEFROM("includefrom", RequiredFolderArgument("absolute path"), RequiredFileArgument("filename", false, false, "tex"), dependency = Package.IMPORT),
    INPUT("input", RequiredFileArgument("sourcefile", true, false, "tex")),
    INPUTFROM("inputfrom", RequiredFolderArgument("absolute path"), RequiredFileArgument("filename", false, false, "tex"), dependency = Package.IMPORT),
    INCLUDEGRAPHICS("includegraphics", "key-val-list".asOptional(), RequiredPicturePathArgument("imagefile", true, false, "pdf", "png", "jpg", "eps", "tikz"), dependency = GRAPHICX),
    INCLUDEONLY("includeonly", RequiredFileArgument("sourcefile", false, false, "tex")),
    INDEXNAME("indexname", "name".asRequired()),
    INDEXSPACE("indexspace"),
    INDEX("intex", "entry".asRequired()),
    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),
    LDOTS("ldots", display = "…"),
    LEFTEQN("lefteqn"),
    LET("let"),
    LFSERIES("lfseries"),
    LINEBREAK("linebreak", "number".asOptional()),
    LINETHICKNESS("linethickness", "dimension".asRequired()),
    LINEWIDTH("linewidth"),
    LISTFIGURENAME("listfigurename", "name".asRequired(Type.TEXT)),
    LISTFILES("listfiles"),
    LISTOFFIGURES("listoffigures"),
    LISTOFTABLES("listoftables"),
    LISTTABLENAME("listtablename", "name".asRequired(Type.TEXT)),
    LOADCLASS("LoadClass", "options".asOptional(), RequiredFileArgument("class", true, false, "cls")),
    LOADCLASSWITHOPTIONS("LoadClassWithOptions", RequiredFileArgument("class", true, false, "cls")),
    LOWERCASE("lowercase", "text".asRequired(Type.TEXT)),
    LQ("lq", display = "‘"),
    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(Type.TEXT)),
    NAMEREF("nameref", "label".asRequired(Type.TEXT), dependency = Package.HYPERREF),
    NEWLABEL("newlabel"),
    NEWLENGTH("newlength", "length".asRequired()),
    NEWLINE("newline"),
    NEWPAGE("newpage"),
    NEWTHEOREM("newtheorem", "envname".asRequired(), "numberedlike".asOptional(), "caption".asRequired(Type.TEXT), "within".asOptional()),
    NEWTHEOREM_STAR("newtheorem*", "envname".asRequired(), "caption".asRequired(Type.TEXT)),
    NOCITE("nocite", "keys".asRequired()),
    NOFILES("nofiles"),
    NOLINEBREAK("nolinebreak", "number".asOptional()),
    NONUMBER("nonumber"),
    NOPAGEBREAK("nopagebreak", "number".asOptional()),
    NOPAGECOLOR("nopagecolor", dependency = XCOLOR),
    NORMALCOLOR("normalcolor"),
    NORMALFONT("normalfont"),
    NORMALSIZE("normalsize"),
    OE("oe", display = "œ"),
    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 = XCOLOR),
    PAGECOLOR2("pagecolor", "model-list".asOptional(), "spec-list".asRequired(), dependency = XCOLOR),
    PAGENAME("pagename"),
    PAGENUMBERING("pagenumbering", "numstyle".asRequired()),
    PAGEREF("pageref", "label".asRequired()),
    PAGESTYLE("pagestyle", "style".asRequired()),
    PAGETOTAL("pagetotal"),
    PAPERWIDTH("paperwidth"),
    PAPERHEIGHT("paperheight"),
    PARAGRAPH("paragraph", "shorttitle".asOptional(Type.TEXT), "title".asRequired(Type.TEXT)),
    PARAGRAPH_STAR("paragraph*", "title".asRequired(Type.TEXT)),
    PARAGRAPHMARK("paragraphmark"),
    PARBOX("parbox", "pos".asOptional(), "width".asRequired(), "text".asRequired(Type.TEXT)),
    PARINDENT("parindent"),
    PARSKIP("parskip"),
    PART("part", "shorttitle".asOptional(Type.TEXT), "title".asRequired(Type.TEXT)),
    PART_STAR("part*", "title".asRequired(Type.TEXT)),
    PARTNAME("partname", "name".asRequired(Type.TEXT)),
    PDFINFO("pdfinfo", "info".asRequired(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()),
    REFNAME("refname", "name".asRequired(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"),
    RMFAMILY("rmfamily"),
    ROMAN("roman", "counter".asRequired()),
    ROTATEBOX("rotatebox", "key-val-list".asOptional(), "degrees".asRequired(), "text".asRequired(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()),
    SCRIPTSIZE("scriptsize"),
    SCSHAPE("scshape"),
    SECTION("section", "shorttitle".asOptional(Type.TEXT), "title".asRequired(Type.TEXT)),
    SECTION_STAR("section*", "title".asRequired(Type.TEXT)),
    SELECTFONT("selectfont"),
    SETCOUNTER("setcounter", "countername".asRequired(), "value".asRequired()),
    SETLENGTH("setlength", "cmd".asRequired(), "length".asRequired()),
    SFFAMILY("sffamily"),
    SHORTSTACK("shortstack", "pos".asOptional(), "text".asRequired(Type.TEXT)),
    SHOWROWCOLORS("showrowcolors", dependency = XCOLOR),
    SLSHAPE("slshape"),
    SMALL("small"),
    SMALLSKIP("smallskip"),
    SMASH("smash"),
    SOUT("sout", "strikethroughtext".asRequired(Type.TEXT), dependency = ULEM),
    SPACE("space"),
    STEPCOUNTER("stepcounter", "counter".asRequired()),
    STOP("stop"),
    STRETCH("stretch", "factor".asRequired()),
    SUBFILE("subfile", RequiredFileArgument("sourcefile", true, false, "tex"), dependency = SUBFILES),
    SUBFILEINCLUDE("subfileinclude", RequiredFileArgument("sourcefile", true, false, "tex"), dependency = SUBFILES),
    SUBIMPORT("subimport", RequiredFolderArgument("relative path"), RequiredFileArgument("filename", false, false, "tex"), dependency = Package.IMPORT),
    SUBINCLUDEFROM("subincludefrom", RequiredFolderArgument("relative path"), RequiredFileArgument("filename", false, false, "tex"), dependency = Package.IMPORT),
    SUBITEM("subitem"),
    SUBPARAGRAPH("subparagraph", "shorttitle".asOptional(Type.TEXT), "title".asRequired(Type.TEXT)),
    SUBPARAGRAPH_STAR("subparagraph*", "title".asRequired(Type.TEXT)),
    SUBPARAGRAPHMARK("subparagraphmark", "code".asRequired()),
    SUBSECTION("subsection", "shorttitle".asOptional(Type.TEXT), "title".asRequired(Type.TEXT)),
    SUBSECTION_STAR("subsection*", "title".asRequired(Type.TEXT)),
    SUBSECTIONMARK("subsectionmark", "code".asRequired()),
    SUBSUBITEM("subsubitem"),
    SUBSUBSECTION("subsubsection", "shorttitle".asOptional(Type.TEXT), "title".asRequired(Type.TEXT)),
    SUBSUBSECTION_STAR("subsubsection*", "title".asRequired(Type.TEXT)),
    SUBSUBSECTIONMARK("subsubsectionmark", "code".asRequired()),
    SUPPRESSFLOATS("suppressfloats", "placement".asOptional()),
    SYMBOL("symbol", "n".asRequired()),
    TABCOLSEP("tabcolsep"),
    TABLENAME("tablename", "name".asRequired(Type.TEXT)),
    TABLEOFCONTENTS("tableofcontents"),
    TESTCOLOR("testcolor", "color".asRequired(), dependency = XCOLOR),
    TESTCOLOR2("testcolor", "model-list".asOptional(), "spec-list".asRequired(), dependency = XCOLOR),
    TEXTASCIICIRCUM("textasciicircum", display = "^"),
    TEXTASCIITILDE("textasciitilde", display = "~"),
    TEXTASTERISKCENTERED("textasciicentered", display = "⁎"),
    TEXTBACKSLASH("textbackslash", display = "\\"),
    TEXTBAR("textbar", display = "|"),
    TEXTBF("textbf", "text".asRequired(Type.TEXT)),
    TEXTBRACELEFT("textbraceleft", display = "{"),
    TEXTBRACERIGHT("textbraceright", display = "}"),
    TEXTBULLET("textbullet", display = "•"),
    TEXTCIRCLED("textcircled", "text".asRequired()),
    TEXTCOLOR("textcolor", "color".asRequired(), "text".asRequired(), dependency = XCOLOR),
    TEXTCOLOR2("textcolor", "model-list".asOptional(), "spec-list".asRequired(), "text".asRequired(), dependency = XCOLOR),
    TEXTCOPYRIGHT("textcopyright", display = "©"),
    TEXTDAGGER("textdagger", display = "†"),
    TEXTDAGGERDBL("textdaggerdbl", display = "‡"),
    TEXTDOLLAR("textdollar", display = "$"),
    TEXTELLIPSIS("textellipsis", display = "…"),
    TEXTEMDASH("textemdash", display = "—"),
    TEXTENDASH("textendash", display = "–"),
    TEXTEXCLAMDOWN("textexclamdown", display = "¡"),
    TEXTGREATER("textgreater", display = ">"),
    TEXTHEIGHT("textheight"),
    TEXTIT("textit", "text".asRequired(Type.TEXT)),
    TEXTLESS("textless"),
    TEXTLF("textlf", "text".asRequired(Type.TEXT)),
    TEXTMD("textmd", "text".asRequired(Type.TEXT)),
    TEXTNORMAL("textnormal"),
    TEXTPARAGRAPH("textparagraph"),
    TEXTPERIODCENTERED("textperiodcentered", display = "·"),
    TEXTQUESTIONDOWN("textquestiondown", display = "¿"),
    TEXTQUOTEDBLLEFT("textquotedblleft", display = "“"),
    TEXTQUOTEDBLRIGHT("textquotedblright", display = "”"),
    TEXTQUOTELEFT("textquoteleft", display = "‘"),
    TEXTQUOTERIGHT("textquoteright", display = "’"),
    TEXTREGISTERED("textregistered", display = "®"),
    TEXTRM("textrm", "text".asRequired(Type.TEXT)),
    TEXTSC("textsc", "textsc".asRequired(Type.TEXT)),
    TEXTSECTION("textsection", display = "§"),
    TEXTSF("textsf", "text".asRequired(Type.TEXT)),
    TEXTSL("textsl", "text".asRequired(Type.TEXT)),
    TEXTSTERLING("textsterling", display = "£"),
    TEXTSUBSCRIPT("textsubscript", "text".asRequired(Type.TEXT)),
    TEXTSUPERSCRIPT("textsuperscript", "text".asRequired(Type.TEXT)),
    TEXTTRADEMARK("texttrademark", display = "™"),
    TEXTTT("texttt", "text".asRequired(Type.TEXT)),
    TEXTUNDERSCORE("textunderscore", display = "_"),
    TEXTUP("textup", "text".asRequired(Type.TEXT)),
    TEXTVISIBLESPACE("textvisiblespace", display = "␣"),
    TEXTWIDTH("textwidth"),
    THANKS("thanks", "to".asRequired(Type.TEXT)),
    THICKLINES("thicklines"),
    THINLINES("thinlines"),
    THISPAGESTYLE("thispagestyle", "style".asRequired()),
    TIME("time"),
    TINY("tiny"),
    TITLE("title", "text".asRequired(Type.TEXT)),
    TODAY("today"),
    TOPMARGIN("topmargin"),
    TTFAMILY("ttfamily"),
    TWOCOLUMN("twocolumn", "text".asOptional(Type.TEXT)),
    UNBOLDMATH("unboldmath"),
    UNDERLINE("underline", "text".asRequired(Type.TEXT)),
    UNITLENGTH("unitlength"),
    UPPERCASE("uppercase", "text".asRequired(Type.TEXT)),
    UPSHAPE("upshape"),
    URL("url", "url".asRequired(), dependency = Package.HYPERREF),
    USEPACKAGE("usepackage", "options".asOptional(), RequiredFileArgument("package", true, true, "sty")),
    VDOTS("vdots", display = "⋮"),
    VLINE("vline"),
    VSPACE("vspace", "length".asRequired()),
    VSPACE_STAR("vspace*", "length".asRequired()),
    WIDTH("width"),
    XGLOBAL("xglobal", dependency = XCOLOR),

    /**
     * New definitions
     */
    NEWCOMMAND("newcommand", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Type.TEXT)),
    NEWCOMMAND_STAR("newcommand*", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Type.TEXT)),
    NEWIF("newif", "cmd".asRequired()),
    PROVIDECOMMAND("providecommand", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Type.TEXT)),
    PROVIDECOMMAND_STAR("providecommand*", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Type.TEXT)),
    RENEWCOMMAND("renewcommand", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Type.TEXT)),
    RENEWCOMMAND_STAR("renewcommand*", "cmd".asRequired(), "args".asOptional(), "default".asOptional(), "def".asRequired(Type.TEXT)),
    NEWENVIRONMENT("newenvironment", "name".asRequired(), "args".asOptional(), "default".asOptional(), "begdef".asRequired(Type.TEXT), "enddef".asRequired(Type.TEXT)),
    RENEWENVIRONMENT("renewenvironment", "name".asRequired(), "args".asOptional(), "default".asOptional(), "begdef".asRequired(Type.TEXT), "enddef".asRequired(Type.TEXT)),

    /**
     * mathtools definitions
     */
    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(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(Type.TEXT), dependency = MATHTOOLS),

    /**
     * xcolor definitions
     */
    DEFINECOLOR("definecolor", "type".asOptional(), "name".asRequired(), "model-list".asRequired(), "spec-list".asRequired(), dependency = XCOLOR),
    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 = XCOLOR),
    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),

    /**
     * Xparse definitions
     */
    NEWDOCUMENTCOMMAND("NewDocumentCommand", "name".asRequired(), "args spec".asRequired(), "code".asRequired(), dependency = Package.XPARSE),
    RENEWDOCUMENTCOMMAND("RenewDocumentCommand", "name".asRequired(), "args spec".asRequired(), "code".asRequired(), dependency = Package.XPARSE),
    PROVIDEDOCUMENTCOMMAND("ProvideDocumentCommand", "name".asRequired(), "args spec".asRequired(), "code".asRequired(), dependency = Package.XPARSE),
    DECLAREDOCUMENTCOMMAND("DeclareDocumentCommand", "name".asRequired(), "args spec".asRequired(), "code".asRequired(), dependency = Package.XPARSE),
    NEWDOCUMENTENVIRONMENT("NewDocumentEnvironment", "name".asRequired(), "args spec".asRequired(), "start code".asRequired(), "end code".asRequired(), dependency = Package.XPARSE),
    RENEWDOCUMENTENVIRONMENT("RenewDocumentEnvironment", "name".asRequired(), "args spec".asRequired(), "start code".asRequired(), "end code".asRequired(), dependency = Package.XPARSE),
    PROVIDEDOCUMENTENVIRONMENT("ProvideDocumentEnvironment", "name".asRequired(), "args spec".asRequired(), "start code".asRequired(), "end code".asRequired(), dependency = Package.XPARSE),
    DECLAREDOCUMENTENVIRONMENT("DeclareDocumentEnvironment", "name".asRequired(), "args spec".asRequired(), "start code".asRequired(), "end code".asRequired(), dependency = Package.XPARSE),

    /**
     * Natbib citations
     */
    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),
    CITEYEARPAR("citeyearpar", "keys".asRequired(), dependency = NATBIB),
    CITENUM("citenum", "key".asRequired(), dependency = NATBIB),
    CITETEXT("citetext", "text".asRequired(), dependency = NATBIB),

    /**
     * Biblatex commands
     */
    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("footcullcite", "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(Type.TEXT), dependency = BIBLATEX),
    BRACKETTEXT("brackettext", "text".asRequired(Type.TEXT), dependency = BIBLATEX),

    /**
     * SIunitx commands
     */
    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),

    /**
     * Algorithmicx
     */
    FOR("For", "condition".asRequired(), dependency = ALGPSEUDOCODE),
    FORALL("ForAll", "condition".asRequired(), dependency = ALGPSEUDOCODE),
    ENDFOR("EndFor", dependency = ALGPSEUDOCODE),
    IF("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);

    companion object {

        private val lookup = HashMap<String, MutableSet<LatexRegularCommand>>()
        private val lookupDisplay = HashMap<String, MutableSet<LatexRegularCommand>>()

        init {
            @Suppress("RemoveRedundantQualifierName")
            for (command in LatexRegularCommand.values()) {
                lookup.getOrPut(command.command) { mutableSetOf() }.add(command)
                if (command.display != null) {
                    lookupDisplay.putIfAbsent(command.display!!, mutableSetOf(command))?.add(command)
                }
            }
        }

        @JvmStatic
        operator fun get(command: String) = lookup[command]?.toSet()

        @JvmStatic
        fun findByDisplay(display: String) = lookupDisplay[display]?.toSet()
    }
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Sten Wessel
 */
@Suppress("ConvertSecondaryConstructorToPrimary")
class OptionalArgument : Argument {

    @JvmOverloads
    internal constructor(name: String, type: Type = Type.NORMAL) : super(name, type)

    override fun toString() = "[$name]"
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Hannah Schellekens
 */
open class Package @JvmOverloads constructor(
        val name: String,
        vararg val parameters: String = emptyArray()
) {

    companion object {

        // Predefined packages.
        @JvmField val DEFAULT = Package("")
        @JvmField val ALGPSEUDOCODE = Package("algpseudocode")
        @JvmField val AMSFONTS = Package("amsfonts")
        @JvmField val AMSMATH = Package("amsmath")
        @JvmField val AMSSYMB = Package("amssymb")
        @JvmField val BIBLATEX = Package("biblatex")
        @JvmField val BM = Package("bm")
        @JvmField val BOOKTABS = Package("booktabs")
        @JvmField val COMMENT = Package("comment")
        @JvmField val CLEVEREF = Package("cleveref")
        @JvmField val CSQUOTES = Package("csquotes")
        @JvmField val FONTENC = Package("fontenc")
        @JvmField val GAUSS = Package("gauss")
        @JvmField val GLOSSARIES = Package("glossaries")
        @JvmField val GLOSSARIESEXTRA = Package("glossaries-extra")
        @JvmField val GRAPHICS = Package("graphics")
        @JvmField val GRAPHICX = Package("graphicx")
        @JvmField val HYPERREF = Package("hyperref")
        @JvmField val IMAKEIDX = Package("imakeidx")
        @JvmField val IMPORT = Package("import")
        @JvmField val INPUTENC = Package("inputenc")
        @JvmField val LATEXSYMB = Package("latexsymb")
        @JvmField val LISTINGS = Package("listings")
        @JvmField val LUACODE = Package("luacode")
        @JvmField val MATHABX = Package("mathabx")
        @JvmField val MATHTOOLS = Package("mathtools")
        @JvmField val NATBIB = Package("natbib")
        @JvmField val SIUNITX = Package("siunitx")
        @JvmField val SUBFILES = Package("subfiles")
        @JvmField val TIKZ = Package("tikz")
        @JvmField val ULEM = Package("ulem")
        @JvmField val XCOLOR = Package("xcolor")
        @JvmField val XPARSE = Package("xparse")
    }

    /**
     * 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)

    /**
     * Creates a new package object with the same name and with the given parameters.
     */
    fun with(vararg parameters: String) = Package(name, *parameters)

    override fun equals(other: Any?): Boolean {
        if (this === other) {
            return true
        }
        if (other !is Package) {
            return false
        }

        val aPackage = other as Package?
        return name == aPackage!!.name
    }

    override fun hashCode() = name.hashCode()

    override fun toString() = "Package{$name}"
}

package nl.hannahsten.texifyidea.lang

/**
 * @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}"
}

package nl.hannahsten.texifyidea.lang

import nl.hannahsten.texifyidea.file.FileExtensionMatcher
import nl.hannahsten.texifyidea.file.FileNameMatcher
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.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())
            return
        }
        else {
            defaultExtension = extensions[0]
        }
        regex.append("(")
        for (extension in extensions) {
            regex.append("\\.")
            val extensionLower = extension.toLowerCase()
            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.toLowerCase()).matches()
    }

    override fun matchesExtension(extension: String): Boolean {
        return supportedExtensions.contains(extension)
    }
}

package nl.hannahsten.texifyidea.lang

/**
 * @author Lukas Heiligenbrunner
 */
class RequiredFolderArgument(name: String?) : RequiredArgument(name!!, Type.FILE)

package nl.hannahsten.texifyidea.lang

/**
 * @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

/**
 * @author Hannah Schellekens
 */
data class SimpleBibtexEntryField(override val fieldName: String, override val description: String, override val dependency: Package = Package.DEFAULT) : BibtexEntryField {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is SimpleBibtexEntryField) return false

        if (fieldName != other.fieldName) return false
        return true
    }

    override fun hashCode() = fieldName.hashCode()
}

package nl.hannahsten.texifyidea.lang

/**
 * @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: Package = Package.DEFAULT
) : 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 mainFilePath = path + File.separator + "main.tex"
        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)

        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 bibFilePath = path + File.separator + "main.bib"
        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)

        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.ModuleBuilder
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.openapi.Disposable
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.roots.CompilerModuleExtension
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.util.Pair
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
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)

    @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)
        }

        // 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 com.intellij.openapi.module.ModuleTypeManager
import nl.hannahsten.texifyidea.TexifyIcons

/**
 * @author Sten Wessel
 */
class LatexModuleType : ModuleType<LatexModuleBuilder>(ID) {

    companion object {

        private const val ID = "LATEX_MODULE_TYPE"

        val INSTANCE: LatexModuleType
            get() = ModuleTypeManager.getInstance().findByID(ID) as 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.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 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<TexifySettings>())

    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.SERVICE.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)
                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.SERVICE.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.navigation

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.findCommandDefinitions
import nl.hannahsten.texifyidea.util.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.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.extractLabelName
import nl.hannahsten.texifyidea.util.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.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.Magic
import nl.hannahsten.texifyidea.util.extractLabelName
import nl.hannahsten.texifyidea.util.forcedFirstRequiredParameterAsCommand
import nl.hannahsten.texifyidea.util.requiredParameter

/**
 * @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 Magic.Command.getLabelDefinitionCommands(psiElement.project)) {
                            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.parser

import com.intellij.lang.PsiBuilder
import com.intellij.lang.parser.GeneratedParserUtilBase
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.Magic

@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 (env !in Magic.Environment.verbatim) 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.project

import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.projectView.ProjectViewNode
import com.intellij.ide.projectView.ProjectViewNodeDecorator
import com.intellij.packageDependencies.ui.PackageDependenciesNode
import com.intellij.ui.ColoredTreeCellRenderer
import nl.hannahsten.texifyidea.util.Magic

/**
 * @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.toLowerCase().endsWith("synctex.gz")) {
                return
            }
        }

        val icon = Magic.Icon.fileIcons[extension.toLowerCase()] ?: return

        presentationData.setIcon(icon)
    }

    override fun decorate(projectViewNode: ProjectViewNode<*>, presentationData: PresentationData) {
        setIcon(projectViewNode, presentationData)
    }

    override fun decorate(packageDependenciesNode: PackageDependenciesNode, coloredTreeCellRenderer: ColoredTreeCellRenderer) {
        // Do nothing.
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.openapi.paths.WebReference
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiReference
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.evaluate

/**
 * Get all the references from a BibTeX entry.
 */
fun getReferences(element: BibtexEntry): Array<PsiReference> {
    val urls = Magic.Command.bibUrls
            .map { element.getTagContent(it) }
            .filter { it.isNotBlank() }

    if (urls.isNotEmpty()) {
        // We cannot simply return urls.map { WebReference(element, 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(element, text range) instead.
        val contentText = element.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(element, it.shiftRight(element.entryContent!!.textOffset - element.textOffset))
        }.toArray(emptyArray())
    }

    return emptyArray()
}

/**
 * Get the content of a tag in a given BibTeX entry.
 */
fun getTagContent(element: BibtexEntry, tagName: String): String {
    val entryContent = element.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.psi

import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileFactory
import nl.hannahsten.texifyidea.BibtexLanguage
import nl.hannahsten.texifyidea.util.firstParentOfType

fun getNameIdentifier(element: BibtexId): PsiElement {
    return element
}

fun setName(element: BibtexId, name: String): PsiElement {
    // Replace the complete bibtex entry to automatically update the index (which is on entries, not ids)
    val entry = element.firstParentOfType(BibtexEntry::class)
    val oldName = element.name ?: return element
    val newText = entry?.text?.replaceFirst(oldName, name) ?: return element
    val newElement = PsiFileFactory.getInstance(element.project).createFileFromText("DUMMY.tex", BibtexLanguage, newText, false, true).firstChild
    entry.parent.node.replaceChild(entry.node, newElement.node)
    return element
}

fun getName(element: BibtexId): String {
    // Drop the , separator
    return element.text.dropLast(1)
}

package nl.hannahsten.texifyidea.psi

import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiReference
import nl.hannahsten.texifyidea.reference.SimpleFileReference
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.firstChildOfType

fun getReferences(element: BibtexTag): Array<PsiReference> {
    val key = element.firstChildOfType(BibtexKey::class)?.text ?: return emptyArray()
    if (key !in Magic.File.bibtexFileKeys) return emptyArray()
    val content = element.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(element, 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(element.text.indexOf(partSplit[1]), filePath.length)
                references.add(SimpleFileReference(element, filePath, textRange))
            }
            else {
                // Assume we already have the filepath
                val filePath = partSplit[0]
                references.add(SimpleFileReference(element, filePath))
            }
        }
    }

    return references.toTypedArray()
}

package nl.hannahsten.texifyidea.psi

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.PsiReference
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.nextLeaf
import com.intellij.util.containers.toArray
import nl.hannahsten.texifyidea.lang.CommandManager
import nl.hannahsten.texifyidea.lang.LatexCommand
import nl.hannahsten.texifyidea.lang.RequiredArgument
import nl.hannahsten.texifyidea.lang.RequiredFileArgument
import nl.hannahsten.texifyidea.reference.CommandDefinitionReference
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.reference.LatexLabelReference
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.requiredParameters
import nl.hannahsten.texifyidea.util.shrink
import java.util.ArrayList
import java.util.LinkedHashMap
import java.util.regex.Pattern

/**
 * Get the references for this command.
 * For example for a \ref{label1,label2} command, then label1 and label2 are the references.
 */
fun getReferences(element: LatexCommands): Array<PsiReference> {
    val firstParam = readFirstParam(element)

    // If it is a reference to a label
    if (Magic.Command.getLabelReferenceCommands(element.project).contains(element.commandToken.text) && firstParam != null) {
        val references = extractLabelReferences(element, firstParam)
        return references.toTypedArray()
    }

    // If it is a reference to a file
    val references: List<PsiReference> = element.getFileArgumentsReferences()
    if (firstParam != null && references.isNotEmpty()) {
        return references.toTypedArray()
    }

    if (Magic.Command.urls.contains(element.name) && firstParam != null) {
        return element.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 reference = CommandDefinitionReference(element)
    // Only create a reference if there is something to resolve to, otherwise autocompletion won't work
    return if (reference.multiResolve(false).isEmpty()) {
        emptyArray()
    }
    else {
        arrayOf(reference)
    }
}

/**
 * Check if the command includes other files, and if so return [InputFileReference] instances for them.
 *
 * Do not use this method directly, use command.references.filterIsInstance<InputFileReference>() instead.
 */
private 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))
        }
    }

    return inputFileReferences
}

/**
 * Create label references from the command parameter given.
 */
fun extractLabelReferences(element: LatexCommands, firstParam: LatexRequiredParam): List<PsiReference> {
    val subParamRanges = extractSubParameterRanges(firstParam)
    val references: MutableList<PsiReference> = ArrayList()
    for (range in subParamRanges) {
        references.add(LatexLabelReference(
                element, range.shiftRight(firstParam.textOffset - element.textOffset)
        ))
    }
    return references
}

fun readFirstParam(element: LatexCommands): LatexRequiredParam? {
    return ApplicationManager.getApplication().runReadAction(Computable {
        val params: List<LatexRequiredParam> = element.requiredParameters()
        if (params.isEmpty()) null else params[0]
    })
}

fun extractSubParameterRanges(param: LatexRequiredParam): List<TextRange> {
    return splitToRanges(stripGroup(param.text), Magic.Pattern.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 getOptionalParameters(parameters: List<LatexParameter>): LinkedHashMap<String, String> {
    val parameterMap = LinkedHashMap<String, String>()
    // 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}]
    val parameterString = parameters.mapNotNull { it.optionalParam }
        // extract the content of each parameter element
        .map { param ->
            param.optionalParamContentList
        }
        .map { contentList ->
            contentList.mapNotNull { content: LatexOptionalParamContent ->
                // the content is either simple text
                val text = content.parameterText
                if (text != null) return@mapNotNull text.text
                // or a group like in param={some value}
                if (content.group == null) return@mapNotNull null
                content.group!!.contentList.joinToString { it.text }
            }
            // Join different content types (like name= and {value}) together without separator
            .joinToString("")
        }
        // Join different parameters (like [param1][param2]) together with separator
        .joinToString(",")

    if (parameterString.trim { it <= ' ' }.isNotEmpty()) {
        for (parameter in parameterString.split(",")) {
            val parts = parameter.split("=".toRegex()).toTypedArray()
            parameterMap[parts[0].trim()] = if (parts.size > 1) parts[1].trim() else ""
        }
    }
    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())

/**
 * Checks if the command is followed by a label.
 */
fun hasLabel(element: LatexCommands): Boolean {
    // Next leaf is a command token, parent is LatexCommands
    val labelMaybe = element.nextLeaf { it !is PsiWhiteSpace }?.parent as? LatexCommands ?: return false
    return CommandManager.labelAliasesInfo.getOrDefault(labelMaybe.commandToken.text, null)?.labelsPreviousCommand == true
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.LatexLanguage
import org.jetbrains.annotations.NonNls

/**
 * @author Sten Wessel
 */
class LatexElementType(@NonNls debugName: String) : IElementType(debugName, LatexLanguage.INSTANCE)

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.LatexLanguage
import nl.hannahsten.texifyidea.psi.impl.LatexEnvironmentImpl

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.environmentName}}") - 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.INSTANCE, 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.CommandManager
import nl.hannahsten.texifyidea.util.Magic

/*
* 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 getLabel(element: LatexEnvironment): String? {
    val stub = element.stub
    if (stub != null) return stub.label
    return if (Magic.Environment.labelAsParameter.contains(element.environmentName)) {
        // See if we can find a label option
        val optionalParameters = getOptionalParameters(element.beginCommand.parameterList)
        optionalParameters.getOrDefault("label", null)
    }
    else {
        if (!Magic.Environment.labeled.containsKey(element.environmentName)) return null
        val content = element.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 = Magic.Command.labelDefinitionsWithoutCustomCommands
            val labelCommand = children.firstOrNull { c: LatexCommands -> labelCommands.contains(c.name) } ?: return null
            val requiredParameters = labelCommand.requiredParameters
            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 getEnvironmentName(element: LatexEnvironment): String? {
    val stub = element.stub
    if (stub != null) return stub.environmentName
    val parameters = element.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.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

/**
 * 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)

            // Allow lstlisting language to be overridden with magic comment
            val languageId = if (!hasMagicCommentKey && host.environmentName == "lstlisting") {
                host.beginCommand.optionalParameters.getOrDefault("language", null)
            }
            else if (hasMagicCommentKey) {
                magicComment.value(DefaultMagicKeys.INJECT_LANGUAGE)
            }
            else {
                return
            }

            if (languageId.isNullOrBlank()) return
            val language = Language.findLanguageByID(languageId)
                    ?: Language.findLanguageByID(languageId.toLowerCase()) ?: return

            val range = host.environmentContent?.textRange?.shiftRight(-host.textOffset) ?: TextRange.EMPTY_RANGE

            registrar.addPlace(language, range, null, null)
        }
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import nl.hannahsten.texifyidea.reference.BibtexIdReference
import nl.hannahsten.texifyidea.reference.LatexEnvironmentReference
import nl.hannahsten.texifyidea.reference.LatexLabelParameterReference
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.extractLabelName
import nl.hannahsten.texifyidea.util.firstParentOfType

/**
 * If the normal text is the parameter of a \ref-like command, get the references to the label declaration.
 */
@Suppress("RemoveExplicitTypeArguments") // Somehow they are needed
fun getReferences(element: LatexParameterText): Array<PsiReference> {
    // If the command is a label reference
    // NOTE When adding options here, also update getNameIdentifier below
    return when {
        Magic.Command.labelReferenceWithoutCustomCommands.contains(element.firstParentOfType(LatexCommands::class)?.name) -> {
            arrayOf<PsiReference>(LatexLabelParameterReference(element))
        }
        // If the command is a bibliography reference
        Magic.Command.bibliographyReference.contains(element.firstParentOfType(LatexCommands::class)?.name) -> {
            arrayOf<PsiReference>(BibtexIdReference(element))
        }
        // If the command is an \end command (references to \begin)
        element.firstParentOfType(LatexEndCommand::class) != null -> {
            arrayOf<PsiReference>(LatexEnvironmentReference(element))
        }
        else -> {
            emptyArray<PsiReference>()
        }
    }
}

/**
 * If [getReferences] returns one reference return that one, null otherwise.
 */
fun getReference(element: LatexParameterText): PsiReference? {
    val references = getReferences(element)
    return if (references.size != 1) {
        null
    }
    else {
        references[0]
    }
}

fun getNameIdentifier(element: LatexParameterText): 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 element is not an identifier
    // It is important not to return null for any identifier, otherwise exceptions like "Throwable: null byMemberInplaceRenamer" may occur
    val name = element.firstParentOfType(LatexCommands::class)?.name
    if (!Magic.Command.labelReferenceWithoutCustomCommands.contains(name) &&
        !Magic.Command.labelDefinitionsWithoutCustomCommands.contains(name) &&
        !Magic.Command.bibliographyReference.contains(name) &&
        element.firstParentOfType(LatexEndCommand::class) == null &&
        element.firstParentOfType(LatexBeginCommand::class) == null) {
        return null
    }
    return element
}

fun setName(element: LatexParameterText, name: String): PsiElement {
    val command = element.firstParentOfType(LatexCommands::class)
    // If we want to rename a label
    if (Magic.Command.reference.contains(command?.name) || Magic.Command.getLabelDefinitionCommands(element.project).contains(command?.name)) {
        // Get a new psi element 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 = element.extractLabelName()
        // This could go wrong in so many cases
        val labelText = command?.text?.replaceFirst(oldLabel, name) ?: "${command?.name}{$name}"
        val newElement = LatexPsiHelper(element.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)
        }
    }
    else if (element.firstParentOfType(LatexEndCommand::class) != null || element.firstParentOfType(LatexBeginCommand::class) != null) {
        // We are renaming an environment, text in \begin or \end
        val newElement = LatexPsiHelper(element.project).createFromText(name).firstChild
        val oldNode = element.node
        val newNode = newElement.node
        if (oldNode == null) {
            element.parent.node.addChild(newNode)
        }
        else {
            element.parent.node.replaceChild(oldNode, newNode)
        }
    }
    // Else, element is not renamable

    return element
}

fun getName(element: LatexParameterText): String {
    return element.text ?: ""
}

package nl.hannahsten.texifyidea.psi

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileFactory
import nl.hannahsten.texifyidea.LatexLanguage
import nl.hannahsten.texifyidea.util.findFirstChild
import nl.hannahsten.texifyidea.util.firstChildOfType

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 createOptionalParameterContent(parameter: String): List<LatexOptionalParamContent> {
        val commandText = "\\begin{lstlisting}[$parameter]"
        val environment = createFromText(commandText).firstChildOfType(LatexEnvironment::class)!!
        val optionalParam = environment.beginCommand.firstChildOfType(LatexOptionalParam::class)!!
        return optionalParam.optionalParamContentList
    }

    fun createFromText(text: String): PsiElement =
            PsiFileFactory.getInstance(project).createFileFromText("DUMMY.tex", LatexLanguage.INSTANCE, 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)!!
    }

    /**
     * Replaces the optional parameter with the supplied name. If a value is supplied, the new parameter will
     * have a value appended with an equal sign (e.g., param={value}). The new parameter has the same position
     * as the old one. If no parameter with the supplied name is found, no action will be performed.
     */
    fun replaceOptionalParameter(parameters: List<LatexParameter>, name: String, newValue: String?) {
        val optionalParam = parameters.first { p -> p.optionalParam != null }.optionalParam!!

        val parameterText = if (newValue != null) {
            "$name={$newValue}"
        }
        else {
            name
        }

        val labelRegex = "label\\s*=\\s*[^,]*".toRegex()
        val elementsToReplace = mutableListOf<LatexOptionalParamContent>()
        val elementIterator = optionalParam.optionalParamContentList.iterator()
        while (elementIterator.hasNext()) {
            val latexContent = elementIterator.next()
            val elementIsLabel = latexContent.parameterText?.text?.contains(labelRegex) ?: false
            if (elementIsLabel) {
                elementsToReplace.add(latexContent)

                // check if the label name is part of the text or in a separate group
                if (latexContent.parameterText!!.text.split("=")[1].trim().isEmpty()) {
                    val group = elementIterator.next()
                    elementsToReplace.add(group)
                }
            }
        }

        val newContents = createOptionalParameterContent(parameterText)
        newContents.forEach { optionalParam.addBefore(it, elementsToReplace.first()) }
        elementsToReplace.forEach { it.delete() }
    }

    /**
     * Add an optional parameter of the form "param" or "param={value}" to the list of optional parameters.
     * If there already are optional parameters, the new parameter will be appended with a "," as the separator.
     *
     * @return A list containing the newly inserted elements from left to right
     */
    fun addOptionalParameter(command: LatexBeginCommand, name: String, value: String?): List<PsiElement> {
        val existingParameters = command.optionalParameters
        if (existingParameters.isEmpty()) {
            command.addAfter(createLatexOptionalParam(), command.parameterList[0])
        }

        val optionalParam = command.parameterList
                .first { p -> p.optionalParam != null }.optionalParam!!

        var parameterText = if (value != null) {
            "$name={$value}"
        }
        else {
            name
        }

        if (existingParameters.isNotEmpty()) {
            parameterText = ",$parameterText"
        }
        val newElements = mutableListOf<PsiElement>()
        val contents = createOptionalParameterContent(parameterText)
        contents.forEach {
            val inserted = optionalParam.addBefore(it, optionalParam.lastChild)
            newElements.add(inserted)
        }
        return newElements
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.lang.ASTNode
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.tree.TokenSet
import java.util.ArrayList

/**
 * @author Hannah Schellekens
 */
object LatexPsiUtil {
    /**
     * Finds the previous sibling of an element but skips over whitespace.
     *
     * @param element
     * 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.
     */
    @JvmStatic
    fun getPreviousSiblingIgnoreWhitespace(element: PsiElement): PsiElement? {
        var sibling: PsiElement? = element
        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.
     *
     * @param element
     * 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.
     */
    @JvmStatic
    fun getNextSiblingIgnoreWhitespace(element: PsiElement): PsiElement? {
        var sibling: PsiElement? = element
        while (sibling?.nextSibling.also { sibling = it } != null) {
            if (sibling !is PsiWhiteSpace) {
                return sibling
            }
        }
        return null
    }

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

    /**
     * Get all the elements of the subtree starting at the given Latex [PsiElement].
     *
     *
     * This method uses a depth-first traversal.
     *
     * @param element
     * The [PsiElement] contained in [nl.hannahsten.texifyidea.psi] of which you
     * want to get all the elements of the subtree of.
     * @return A list of all elements in the subtree starting at, and including, the given element
     * at index 0. The list will be empty when the element has no children or when the element is
     * not a Latex element.
     */
    fun getAllChildren(element: PsiElement): List<PsiElement> {
        return getAllChildren(ArrayList(), element)
    }

    /**
     * See [LatexPsiUtil.getAllChildren], but appends all children to the given
     * list.
     */
    private fun getAllChildren(result: MutableList<PsiElement>, element: PsiElement): List<PsiElement> {
        result.add(element)
        for (child in getChildren(element)) {
            getAllChildren(result, child)
        }
        return result
    }

    /**
     * Get all the Latex children of the given Latex [PsiElement]s.
     *
     * @param element
     * The [PsiElement] contained in [nl.hannahsten.texifyidea.psi] of which you
     * want to get all children of.
     * @return A list of all children of the given element. The list will be empty when the element
     * has no children or when the element is not a Latex element.
     */
    fun getChildren(element: PsiElement?): List<PsiElement> {
        val result: MutableList<PsiElement?> = ArrayList()

        // LatexCommands
        when (element) {
            is LatexCommands -> {
                result.addAll(element.parameterList)
                result.add(element.commandToken)
            }
            is LatexComment -> {
                result.add(element.commentToken)
            }
            is LatexContent -> {
                result.add(element.noMathContent)
            }
            is LatexDisplayMath -> {
                result.add(element.mathContent)
            }
            is LatexGroup -> {
                result.addAll(element.contentList)
            }
            is LatexInlineMath -> {
                result.add(element.mathContent)
            }
            is LatexMathEnvironment -> {
                result.add(element.displayMath)
                result.add(element.inlineMath)
            }
            is LatexNoMathContent -> {
                result.add(element.commands)
                result.add(element.comment)
                result.add(element.group)
                //            result.add(noMathContent.getOpenGroup());
                result.add(element.normalText)
            }
            is LatexOptionalParamContent -> {
                result.add(element.group)
                result.add(element.parameterText)
            }
            is LatexRequiredParamContent -> {
                result.add(element.group)
                result.add(element.parameterText)
            }
            is LatexOptionalParam -> {
                result.addAll(element.optionalParamContentList)
            }
            is LatexParameter -> {
                result.add(element.optionalParam)
                result.add(element.requiredParam)
            }
            is LatexRequiredParam -> {
                result.addAll(element.requiredParamContentList)
            }
            is LatexMathContent -> {
                result.addAll(element.noMathContentList)
            }
        }
        return result.filterNotNull()
    }

    /**
     * Returns whether the node has one of the element types specified in the token set.
     */
    fun hasElementType(node: ASTNode, set: TokenSet): Boolean {
        return set.contains(node.elementType)
    }
}

package nl.hannahsten.texifyidea.psi

import com.intellij.psi.tree.IElementType
import nl.hannahsten.texifyidea.LatexLanguage
import org.jetbrains.annotations.NonNls

/**
 * @author Sten Wessel
 */
class LatexTokenType(@NonNls debugName: String) : IElementType(debugName, LatexLanguage.INSTANCE) {

    override fun toString() = "LatexTokenType." + super.toString()
}

package nl.hannahsten.texifyidea.psi

import com.intellij.lang.ASTNode
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.psi.*
import com.intellij.psi.scope.PsiScopeProcessor
import nl.hannahsten.texifyidea.folding.LatexImportFoldingBuilder
import nl.hannahsten.texifyidea.util.endOffset
import javax.swing.Icon

/**
 * PsiElement that contains all elements from a given start element to an end element.
 *
 * Most methods only apply to the start node.
 *
 * ***Warning:*** This is a hacky solution. In other words, not all functionality is guaranteed to work like
 * you would expect. I made this to be used in [LatexImportFoldingBuilder] to fold multiple psi elements.
 * And frankly, for that purpose it works just fine.
 * If you want to use it for other purposes: test it thoroughly and add/modify the functionality you need.
 * Use at your own risk.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * @author Hannah Schellekens
 */
open class PsiContainer(val start: PsiElement, val end: PsiElement) : PsiElement {

    fun elements() = generateSequence(start) { it -> it.nextSibling?.takeIf { it != end } }

    /**
     * Only returns `true` when `another`
     */
    override fun isEquivalentTo(another: PsiElement?) = (start == end) && (another == start)

    override fun addBefore(element: PsiElement, anchor: PsiElement?): PsiElement? = start.addBefore(element, anchor)

    override fun copy() = PsiContainer(start, end)

    override fun getText() = elements().joinToString(separator = "") { it.text }

    override fun getStartOffsetInParent() = start.startOffsetInParent

    override fun getPrevSibling(): PsiElement? = start.prevSibling

    override fun <T : Any?> putUserData(key: Key<T>, value: T?) {
        elements().forEach { it.putUserData(key, value) }
    }

    /**
     * Only replaces start element.
     */
    override fun replace(newElement: PsiElement): PsiElement = start.replace(newElement)

    override fun getContainingFile(): PsiFile? = start.containingFile

    override fun getReferences(): Array<PsiReference> = elements().flatMap { it.references.asSequence() }.toList().toTypedArray()

    @Deprecated("Overrides deprecated member.")
    override fun checkAdd(element: PsiElement) = elements().forEach {
        @Suppress("DEPRECATION") // Has to be overridden
        it.checkAdd(element)
    }

    override fun getLanguage() = start.language

    override fun addRangeAfter(first: PsiElement?, last: PsiElement?, anchor: PsiElement?) = null

    override fun getUseScope() = start.useScope

    override fun getResolveScope() = start.resolveScope

    override fun getProject() = start.project

    override fun addRange(first: PsiElement?, last: PsiElement?) = start

    override fun getContext() = start.context

    override fun addAfter(element: PsiElement, anchor: PsiElement?): PsiElement? = end.addAfter(element, anchor)

    override fun processDeclarations(processor: PsiScopeProcessor, state: ResolveState, lastParent: PsiElement?, place: PsiElement): Boolean {
        return elements().all { it.processDeclarations(processor, state, lastParent, place) }
    }

    override fun accept(visitor: PsiElementVisitor) = elements().forEach { it.accept(visitor) }

    override fun getNextSibling(): PsiElement? = end.nextSibling

    override fun getFirstChild(): PsiElement = start.firstChild

    override fun getTextRange() = TextRange(start.textOffset, end.endOffset())

    override fun <T : Any?> putCopyableUserData(key: Key<T>?, value: T?) = elements().forEach {
        it.putCopyableUserData(key, value)
    }

    override fun getOriginalElement(): PsiElement? = start.originalElement

    @Deprecated("Overrides deprecated member.")
    override fun checkDelete() = elements().forEach {
        @Suppress("DEPRECATION") // Has to be overridden
        it.checkDelete()
    }

    override fun getNavigationElement(): PsiElement? = start.navigationElement

    override fun findElementAt(offset: Int) = containingFile?.findElementAt(start.textOffset + offset)

    override fun getReference() = start.reference

    override fun getTextLength() = end.endOffset() - start.textOffset

    override fun textMatches(text: CharSequence) = text == getText()

    override fun textMatches(element: PsiElement) = textMatches(element.text)

    override fun getTextOffset() = start.textOffset

    override fun textToCharArray() = text.toCharArray()

    override fun add(element: PsiElement): PsiElement? = end.add(element)

    override fun addRangeBefore(first: PsiElement, last: PsiElement, anchor: PsiElement?): PsiElement = start.addRangeBefore(first, last, anchor)

    override fun isPhysical() = start.isPhysical

    override fun findReferenceAt(offset: Int) = start.findReferenceAt(offset)

    override fun getNode(): ASTNode? = start.node

    override fun getManager(): PsiManager? = start.manager

    override fun isValid() = start.isValid

    override fun delete() = elements().forEach {
        it.delete()
    }

    override fun getIcon(flags: Int): Icon? = start.getIcon(flags)

    override fun deleteChildRange(first: PsiElement?, last: PsiElement?) = elements().forEach {
        it.deleteChildRange(first, last)
    }

    override fun getParent(): PsiElement? = start.parent

    override fun getChildren(): Array<PsiElement> = emptyArray()

    override fun acceptChildren(visitor: PsiElementVisitor) = elements().forEach {
        it.accept(visitor)
    }

    override fun isWritable() = elements().all { it.isWritable }

    override fun <T : Any?> getUserData(key: Key<T>): T? = start.getUserData(key)

    override fun getLastChild(): PsiElement? = end.lastChild

    override fun textContains(c: Char) = text.contains(c)

    override fun <T : Any?> getCopyableUserData(key: Key<T>?): T? = start.getCopyableUserData(key)
}

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
import com.intellij.psi.impl.PsiModificationTrackerImpl

/**
 * @author Hannah Schellekens
 */
class StructurePsiChangeListener(project: Project) : PsiTreeChangeListener {

    private val psiModificationTracker = PsiManager
            .getInstance(project)
            .modificationTracker as PsiModificationTrackerImpl

    private fun updateTracker() {
        psiModificationTracker.incOutOfCodeBlockModificationCounter()
    }

    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

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

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.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
        // For now we assume the user is refactoring a label
        /** See [formatAsLabel] */
        return name.toSet().intersect(setOf('%', '~', '#', '\\')).isEmpty()
    }
}

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 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.extractLabelName
import nl.hannahsten.texifyidea.util.firstChildOfType

/**
 * 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.childrenOfType
import nl.hannahsten.texifyidea.util.tags
import nl.hannahsten.texifyidea.util.toTextRange
import nl.hannahsten.texifyidea.util.tokenName

/**
 * @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()?.toLowerCase() == "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.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.util.Magic
import nl.hannahsten.texifyidea.util.childrenOfType
import nl.hannahsten.texifyidea.util.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 = Magic.Command.commandDefinitions + Magic.Command.redefinitions

        // Don't resolve to a definition when you are in a \newcommand
        if (element.parentsOfType<LatexCommands>().any { it.name in definitionsAndRedefinitions }) {
            return emptyArray()
        }
        else {
            return LatexDefinitionIndex.getCommandsByNames(definitionsAndRedefinitions, element.project, element.project.projectSearchScope)
                    .filter { it.requiredParameters.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.childrenOfType<LatexCommands>().firstOrNull()

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

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.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.run.latex.LatexDistribution
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.files.*
import java.io.File

/**
 * 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.
 *
 * @author Abby Berkers
 */
class InputFileReference(element: LatexCommands, val range: TextRange, val extensions: Set<String>, val defaultExtension: String) : PsiReferenceBase<LatexCommands>(element) {
    init {
        rangeInElement = range
    }

    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.
     */
    fun resolve(lookForInstalledPackages: Boolean, givenRootFile: VirtualFile? = null): 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 = if (element.name == "\\" + LatexRegularCommand.INCLUDEGRAPHICS.command) {
            getGraphicsPaths(element.project)
        }
        else {
            emptyList()
        }.toMutableList()

        // Find the sources root of the current file.
        // findRootFile will also call getImportPaths, so that will be executed twice
        val rootFile = givenRootFile ?: element.containingFile.findRootFile().virtualFile
        val rootDirectory = rootFile.parent ?: return null

        var targetFile = searchFileByImportPaths(element)?.virtualFile

        // Check environment variables
        val runManager = RunManagerImpl.getInstanceImpl(element.project) as RunManager
        val texInputPath = runManager.allConfigurationsList
                .filterIsInstance<LatexRunConfiguration>()
                .firstOrNull { it.mainFile == rootFile }
                ?.environmentVariables
                ?.envs
                ?.getOrDefault("TEXINPUTS", 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 })
                }
            }
        }

        // Try to find the target file directly from the given path
        if (targetFile == null) {
            targetFile = rootDirectory.findFile(key, extensions)
        }

        // Try content roots
        if (targetFile == null && LatexDistribution.isMiktexAvailable) {
            for (moduleRoot in ProjectRootManager.getInstance(element.project).contentSourceRoots) {
                targetFile = moduleRoot.findFile(key, extensions)
                if (targetFile != null) break
            }
        }

        // Try search paths
        if (targetFile == null) {
            for (searchPath in searchPaths) {
                val path = if (!searchPath.endsWith("/")) "$searchPath/" else searchPath
                targetFile = rootDirectory.findFile(path + key, extensions)
                if (targetFile != null) break
            }
        }

        // Look for packages elsewhere using the kpsewhich command.
        @Suppress("RemoveExplicitTypeArguments")
        if (targetFile == null && lookForInstalledPackages && Magic.Command.includeOnlyExtensions.getOrDefault(element.name, emptySet<String>()).intersect(setOf("sty", "cls")).isNotEmpty()) {
            targetFile = element.getFileNameWithExtensions(key)
                    ?.map { LatexPackageLocationCache.getPackageLocation(it) }
                    ?.map { getExternalFile(it ?: return null) }
                    ?.firstOrNull { it != null }
        }

        if (targetFile == null) return null

        // Return a reference to the target file.
        return PsiManager.getInstance(element.project).findFile(targetFile)
    }

    /**
     * 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(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 = myElement?.node
        val defaultNewText = "${myElement?.name}{$newElementName}"
        // Assumes that it is the last parameter, but at least leaves the options intact
        val default = oldNode?.text?.replaceAfterLast('{', "$newElementName}", defaultNewText) ?: defaultNewText

        // Recall that \ is a file separator on Windows
        val newText = if (elementNameIsJustFilename) {
            oldNode?.text?.trimStart('\\')?.replaceAfterLast(File.separator, "$newElementName}", default.trimStart('\\'))
                ?.let { "\\" + it } ?: default
        }
        else {
            default
        }
        val newNode = LatexPsiHelper(element.project).createFromText(newText).firstChild.node ?: return myElement
        if (oldNode == null) {
            myElement?.parent?.node?.addChild(newNode)
        }
        else {
            myElement.parent.node.replaceChild(oldNode, newNode)
        }
        return myElement
    }

    override fun handleElementRename(newElementName: String): PsiElement {
        return handleElementRename(newElementName, true)
    }

    /**
     * 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): HashSet<String>? {
        val extension: HashSet<String>? = Magic.Command.includeOnlyExtensions[this.commandToken.text]
        return extension?.map { "$fileName.$it" }?.toHashSet()
    }
}

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.firstChildOfType
import nl.hannahsten.texifyidea.util.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.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.extractLabelElement
import nl.hannahsten.texifyidea.util.extractLabelName
import nl.hannahsten.texifyidea.util.findLatexLabelPsiElementsInFileSetAsSequence

/**
 * 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> {
        // Find the label definition
        return myElement.containingFile.findLatexLabelPsiElementsInFileSetAsSequence()
                .filter { it.extractLabelName() == myElement.name }
                .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.Magic
import nl.hannahsten.texifyidea.util.extractLabelName
import nl.hannahsten.texifyidea.util.findBibtexItems
import nl.hannahsten.texifyidea.util.findLabelsInFileSetAsCollection
import java.util.Objects

/**
 * A reference to a label, used only for autocompletion. 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.commandToken.text

        // add bibreferences to autocompletion for \cite-style commands
        if (Magic.Command.bibliographyReference.contains(command)) {
            return file.findBibtexItems().stream()
                    .map { bibtexEntry: PsiElement? ->
                        if (bibtexEntry != null) {
                            val containing = bibtexEntry.containingFile
                            if (bibtexEntry is LatexCommands) {
                                val parameters = bibtexEntry.requiredParameters
                                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 (Magic.Command.getLabelReferenceCommands(element.project).contains(command)) {
            return file.findLabelsInFileSetAsCollection()
                    .stream()
                    .filter { it.extractLabelName().isNotBlank() }
                    .map { labelingCommand: PsiElement ->
                        LookupElementBuilder
                                .create(labelingCommand.extractLabelName())
                                .bold()
                                .withInsertHandler(LatexReferenceInsertHandler())
                                .withTypeText(labelingCommand.containingFile.name + ":" +
                                        (1 + StringUtil.offsetToLineNumber(
                                        labelingCommand.containingFile.text,
                                        labelingCommand.textOffset)), true)
                                .withIcon(TexifyIcons.DOT_LABEL)
                    }.toArray()
        }
        // 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.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.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.
 */
class FileCleanupListener(private val filesToCleanUp: MutableList<File>) : 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()
    }

    override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
    }
}

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("OpenCustomPdfViewerListener", "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.components.JBCheckBox
import com.intellij.ui.components.JBLabel
import nl.hannahsten.texifyidea.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer
import nl.hannahsten.texifyidea.util.Magic
import java.awt.Component
import java.awt.FlowLayout
import javax.swing.*

/**
 * @author Hannah Schellekens, Sten Wessel
 */
@Suppress("SameParameterValue")
class TexifyConfigurable : SearchableConfigurable {

    private val settings: TexifySettings = TexifySettings.getInstance()

    private lateinit var automaticSecondInlineMathSymbol: JBCheckBox
    private lateinit var automaticUpDownBracket: JBCheckBox
    private lateinit var automaticItemInItemize: JBCheckBox
    private lateinit var automaticDependencyCheck: JBCheckBox
    private lateinit var autoCompile: JBCheckBox
    private lateinit var continuousPreview: JBCheckBox
    private lateinit var includeBackslashInSelection: JBCheckBox
    private lateinit var showPackagesInStructureView: JBCheckBox
    private lateinit var automaticQuoteReplacement: ComboBox<String>
    private lateinit var missingLabelMinimumLevel: ComboBox<LatexRegularCommand>
    private lateinit var pdfViewer: ComboBox<String>

    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)")
                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)")
                automaticQuoteReplacement = addSmartQuotesOptions("Off", "TeX ligatures", "TeX commands", "csquotes")
                missingLabelMinimumLevel = addMissingLabelMinimumLevel()
                pdfViewer = addPdfViewerOptions()
            })
        }
    }

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

    private fun JPanel.addMissingLabelMinimumLevel(): ComboBox<LatexRegularCommand> {
        val list = ComboBox(Magic.Command.labeledLevels.keys.toTypedArray())
        add(JPanel(FlowLayout(FlowLayout.LEFT)).apply {
            add(JBLabel("Minimum sectioning level which should trigger the missing label inspection: "))
            add(list)
        })
        list.renderer = object : DefaultListCellRenderer() {
            override fun getListCellRendererComponent(list: JList<*>?, value: Any?, index: Int, isSelected: Boolean, celHasFocus: Boolean): Component {
                val item = (value as? LatexRegularCommand)?.command ?: value
                return super.getListCellRendererComponent(list, item, index, isSelected, celHasFocus)
            }
        }
        return list
    }

    private fun JPanel.addPdfViewerOptions(): ComboBox<String> {
            val availableViewers = PdfViewer.availableSubset().map { it.displayName }.toTypedArray()
            val list = ComboBox(availableViewers)
            add(JPanel(FlowLayout(FlowLayout.LEFT)).apply {
                add(JBLabel("PDF viewer: "))
                add(list)
            })
            return list
    }

    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 automaticSecondInlineMathSymbol.isSelected != settings.automaticSecondInlineMathSymbol ||
                automaticUpDownBracket.isSelected != settings.automaticUpDownBracket ||
                automaticItemInItemize.isSelected != settings.automaticItemInItemize ||
                automaticDependencyCheck.isSelected != settings.automaticDependencyCheck ||
                autoCompile.isSelected != settings.autoCompile ||
                continuousPreview.isSelected != settings.continuousPreview ||
                includeBackslashInSelection.isSelected != settings.includeBackslashInSelection ||
                showPackagesInStructureView.isSelected != settings.showPackagesInStructureView ||
                automaticQuoteReplacement.selectedIndex != settings.automaticQuoteReplacement.ordinal ||
                missingLabelMinimumLevel.selectedItem != settings.missingLabelMinimumLevel ||
                pdfViewer.selectedIndex != settings.pdfViewer.ordinal
    }

    override fun apply() {
        settings.automaticSecondInlineMathSymbol = automaticSecondInlineMathSymbol.isSelected
        settings.automaticUpDownBracket = automaticUpDownBracket.isSelected
        settings.automaticItemInItemize = automaticItemInItemize.isSelected
        settings.automaticDependencyCheck = automaticDependencyCheck.isSelected
        settings.autoCompile = autoCompile.isSelected
        settings.continuousPreview = continuousPreview.isSelected
        settings.includeBackslashInSelection = includeBackslashInSelection.isSelected
        settings.showPackagesInStructureView = showPackagesInStructureView.isSelected
        settings.automaticQuoteReplacement = TexifySettings.QuoteReplacement.values()[automaticQuoteReplacement.selectedIndex]
        settings.missingLabelMinimumLevel = missingLabelMinimumLevel.selectedItem as LatexRegularCommand
        settings.pdfViewer = PdfViewer.availableSubset()[pdfViewer.selectedIndex]
    }

    override fun reset() {
        automaticSecondInlineMathSymbol.isSelected = settings.automaticSecondInlineMathSymbol
        automaticUpDownBracket.isSelected = settings.automaticUpDownBracket
        automaticItemInItemize.isSelected = settings.automaticItemInItemize
        automaticDependencyCheck.isSelected = settings.automaticDependencyCheck
        autoCompile.isSelected = settings.autoCompile
        continuousPreview.isSelected = settings.continuousPreview
        includeBackslashInSelection.isSelected = settings.includeBackslashInSelection
        showPackagesInStructureView.isSelected = settings.showPackagesInStructureView
        automaticQuoteReplacement.selectedIndex = settings.automaticQuoteReplacement.ordinal
        missingLabelMinimumLevel.selectedItem = settings.missingLabelMinimumLevel
        pdfViewer.selectedIndex = PdfViewer.availableSubset().indexOf(settings.pdfViewer)
    }
}

package nl.hannahsten.texifyidea.settings

import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.components.JBLabel
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import java.awt.FlowLayout
import javax.swing.BoxLayout
import javax.swing.JPanel

/**
 * Project-level settings UI.
 *
 * The project settings are a property so they will differ per project.
 */
class TexifyProjectConfigurable : SearchableConfigurable {

    private val projectSettings: TexifyProjectSettings = TexifyProjectSettings()

    private lateinit var compilerCompatibility: ComboBox<LatexCompiler>

    override fun getId() = "TexifyProjectConfigurable"

    override fun getDisplayName() = "Project Settings"

    override fun createComponent() = JPanel(FlowLayout(FlowLayout.LEFT)).apply {
        add(JPanel().apply {
            layout = BoxLayout(this, BoxLayout.Y_AXIS)

            compilerCompatibility = addCompilerCompatibility()
        })
    }

    /**
     * Add the options for the compiler compatibility.
     */
    private fun JPanel.addCompilerCompatibility(): ComboBox<LatexCompiler> {
        // Show available compilers
        val list = ComboBox(LatexCompiler.values())
        add(JPanel(FlowLayout(FlowLayout.LEFT)).apply {
            add(JBLabel("Check for compatibility with compiler: "))
            add(list)
        })
        return list
    }

    override fun isModified(): Boolean {
        return compilerCompatibility.selectedItem != projectSettings.compilerCompatibility
    }

    override fun apply() {
        projectSettings.compilerCompatibility = compilerCompatibility.selectedItem as LatexCompiler
    }

    override fun reset() {
        super.reset()
        compilerCompatibility.selectedItem = projectSettings.compilerCompatibility
    }
}

package nl.hannahsten.texifyidea.settings

import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.XmlSerializerUtil
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler

/**
 * 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))])
data class TexifyProjectSettings(
        var compilerCompatibility: LatexCompiler = LatexCompiler.PDFLATEX
) : PersistentStateComponent<TexifyProjectSettings> {

    companion object {
        @JvmStatic
        fun getInstance(project: Project): TexifyProjectSettings = ServiceManager.getService(project, 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.components.PersistentStateComponent
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import nl.hannahsten.texifyidea.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

/**
 * @author Sten Wessel
 */
@State(name = "TexifySettings", storages = [(Storage("texifySettings.xml"))])
class TexifySettings : PersistentStateComponent<TexifySettingsState> {

    companion object {
        @JvmStatic
        fun getInstance(): TexifySettings = ServiceManager.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 continuousPreview = false
    var includeBackslashInSelection = false
    var showPackagesInStructureView = false
    var automaticQuoteReplacement = QuoteReplacement.NONE
    var missingLabelMinimumLevel = LatexRegularCommand.SUBSECTION
    var pdfViewer = PdfViewer.values().first { it.isAvailable() }

    override fun getState(): TexifySettingsState? {
        return TexifySettingsState(
                automaticSecondInlineMathSymbol = automaticSecondInlineMathSymbol,
                automaticUpDownBracket = automaticUpDownBracket,
                automaticItemInItemize = automaticItemInItemize,
                automaticDependencyCheck = automaticDependencyCheck,
                autoCompile = autoCompile,
                continuousPreview = continuousPreview,
                includeBackslashInSelection = includeBackslashInSelection,
                showPackagesInStructureView = showPackagesInStructureView,
                automaticQuoteReplacement = automaticQuoteReplacement,
                missingLabelMinimumLevel = missingLabelMinimumLevel,
                pdfViewer = pdfViewer
        )
    }

    override fun loadState(state: TexifySettingsState) {
        automaticSecondInlineMathSymbol = state.automaticSecondInlineMathSymbol
        automaticUpDownBracket = state.automaticUpDownBracket
        automaticItemInItemize = state.automaticItemInItemize
        automaticDependencyCheck = state.automaticDependencyCheck
        autoCompile = state.autoCompile
        continuousPreview = state.continuousPreview
        includeBackslashInSelection = state.includeBackslashInSelection
        showPackagesInStructureView = state.showPackagesInStructureView
        automaticQuoteReplacement = state.automaticQuoteReplacement
        missingLabelMinimumLevel = state.missingLabelMinimumLevel
        pdfViewer = state.pdfViewer
    }
}

package nl.hannahsten.texifyidea.settings

import nl.hannahsten.texifyidea.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

data class TexifySettingsState(
        var automaticSecondInlineMathSymbol: Boolean = true,
        var automaticUpDownBracket: Boolean = true,
        var automaticItemInItemize: Boolean = true,
        var automaticDependencyCheck: Boolean = true,
        var autoCompile: Boolean = false,
        var continuousPreview: Boolean = false,
        var includeBackslashInSelection: Boolean = false,
        var showPackagesInStructureView: Boolean = false,
        var automaticQuoteReplacement: TexifySettings.QuoteReplacement = TexifySettings.QuoteReplacement.NONE,
        var missingLabelMinimumLevel: LatexRegularCommand = LatexRegularCommand.SUBSECTION,
        var pdfViewer: PdfViewer = PdfViewer.firstAvailable()
)

package nl.hannahsten.texifyidea.startup

import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity

/**
 * Register a menu item programmatically instead of in plugin.xml, so we can customize when it is shown or not (xml is preferred, but we cannot switch on application name in xml).
 *
 * @author Thomas Schouten
 */
class AnalyzeMenuRegistration : StartupActivity, DumbAware {

    @Synchronized
    override fun runActivity(project: Project) {
        // Get the group which should be added to either the Analyze menu or something else
        val latexAnalyzeMenuGroup = ActionManager.getInstance().getAction("texify.LatexMenuAnalyze") as DefaultActionGroup

        val analyzeGroup = getAnalyzeGroup()

        // Add the group which contains the LaTeX actions to the Analyze menu
        // First remove it, to avoid adding it twice
        analyzeGroup.remove(latexAnalyzeMenuGroup)
        analyzeGroup.add(latexAnalyzeMenuGroup)
    }

    /**
     * The Analyze menu group is not present in PyCharm and probably also not in other IDEs, so we will only use it in IntelliJ.
     * In PyCharm (and others) we will use the Code group.
     */
    private fun getAnalyzeGroup(): DefaultActionGroup {
        val applicationName = ApplicationNamesInfo.getInstance().scriptName

        return if (applicationName == "idea") {
            // Get an instance of the Analyze action group by ID
            ActionManager.getInstance().getAction(IdeActions.GROUP_ANALYZE) as DefaultActionGroup
        }
        else {
            // Get an instance of the Code action group by ID
            ActionManager.getInstance().getAction("CodeMenu") as DefaultActionGroup
        }
    }

    fun unload() {
        val latexAnalyzeMenuGroup = ActionManager.getInstance().getAction("texify.LatexMenuAnalyze") as DefaultActionGroup
        val analyzeGroup = getAnalyzeGroup()
        analyzeGroup.remove(latexAnalyzeMenuGroup)
    }
}

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.editor.ControlTracker
import nl.hannahsten.texifyidea.editor.ShiftTracker
import nl.hannahsten.texifyidea.run.linuxpdfviewer.evince.EvinceInverseSearchListener

class LoadUnloadListener : DynamicPluginListener {
    override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) {
        super.beforePluginUnload(pluginDescriptor, isUpdate)
        AnalyzeMenuRegistration().unload()
        ControlTracker.unload()
        ShiftTracker.unload()
        runBlocking { EvinceInverseSearchListener.unload() }
    }
}

package nl.hannahsten.texifyidea.startup

import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import com.intellij.openapi.util.SystemInfo
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer
import nl.hannahsten.texifyidea.run.linuxpdfviewer.evince.EvinceInverseSearchListener
import nl.hannahsten.texifyidea.settings.TexifySettings

/**
 * @author Sten Wessel
 */
class StartEvinceInverseSearchListener : StartupActivity, DumbAware {

    override fun runActivity(project: Project) {
        if (SystemInfo.isLinux && TexifySettings.getInstance().pdfViewer == PdfViewer.EVINCE) {
            EvinceInverseSearchListener.start(project)
        }
    }
}

package nl.hannahsten.texifyidea.startup

import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import nl.hannahsten.texifyidea.run.latex.LatexDistribution
import nl.hannahsten.texifyidea.util.TexLivePackages
import nl.hannahsten.texifyidea.util.runCommand

class TexLivePackageListInitializer : StartupActivity {
    override fun runActivity(project: Project) {
        if (LatexDistribution.isTexliveAvailable) {
            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.EverywhereContextType
import com.intellij.codeInsight.template.TemplateActionContext
import com.intellij.codeInsight.template.TemplateContextType
import nl.hannahsten.texifyidea.file.LatexFile
import nl.hannahsten.texifyidea.util.inMathContext

/**
 * Defines a LaTeX template context, used to define in which context
 * a live template is available.
 *
 * @author Abby Berkers
 */
open class LatexContext(
        id: String,
        name: String,
        baseContextType: Class<out TemplateContextType>
) : TemplateContextType(id, name, baseContextType) {

    override fun isInContext(context: TemplateActionContext): Boolean =
            context.file is LatexFile

    class Generic : LatexContext("LATEX", "LaTeX", EverywhereContextType::class.java)

    open class LatexMathContext : LatexContext("LATEX_MATH", "Math", Generic::class.java) {

        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)

            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): String {
            val templateManager = FileTemplateManager.getInstance(project)
            val template = templateManager.getInternalTemplate(templateName)
            val properties = Properties(templateManager.defaultProperties)

            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.
 * @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)
            // 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.
                newFileFullPath = "$path/${nameField.text.substring(pathEndIndex + 1).formatAsFileName()}"
                        .replace(Regex("(\\.tex)+$", RegexOption.IGNORE_CASE), "")
            }
        }
    }
}

package nl.hannahsten.texifyidea.ui

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.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)

        val ratio = min(width.toDouble() / imageWidth, height.toDouble() / imageHeight)

        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.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 Okular.
 *
 * @author Abby Berkers
 */
class OkularConfigureInverseSearchDialog {
    init {
        DialogBuilder().apply {
            setTitle("Configure inverse search")

            val body = JLabel("In the Okular settings, go to the Editor tab. Select the Custom Text Editor as Editor, and as a command provide 'idea --line %l'

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 java.awt.Component
import javax.swing.DefaultListCellRenderer
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.ListCellRenderer
import javax.swing.border.EmptyBorder

/**
 * Custom cell renderer to be used for popup choosers.
 * Adds some indentation to the list cell.
 *
 * @author Abby Berkers
 */
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 = EmptyBorder(1, 8, 1, 1)
        return renderer
    }
}

package nl.hannahsten.texifyidea.ui

import java.awt.image.BufferedImage
import javax.swing.*

/**
 * @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.ui

import com.intellij.openapi.util.SystemInfo
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import nl.hannahsten.texifyidea.util.SystemEnvironment
import java.io.File
import java.io.IOException
import java.io.PrintWriter
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import javax.imageio.ImageIO
import javax.swing.SwingUtilities.invokeLater

/**
 * @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()

    /**
     * Modify this variable to include more packages.
     */
    var preamble = defaultPreamble

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

    /**
     * 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) {
        previewForm.setEquation(previewCode)

        // 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.
        fun setPreviewCodeInTemp(tempDirectory: File) {
            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 = runCommand(
                    "pdflatex",
                    arrayOf(
                        "-interaction=nonstopmode",
                        "-halt-on-error",
                        "$tempBasename.tex"
                    ),
                    tempDirectory
                ) ?: return

                runInkscape(tempBasename, tempDirectory)

                val image = ImageIO.read(File("$tempBasename.png"))
                invokeLater {
                    previewForm.setPreview(image, latexStdoutText)
                }
            }
            finally {
                // Delete all the created temp files in the default temp directory.
                tempDirectory.deleteRecursively()
            }
        }

        GlobalScope.launch {
            try {
                // Snap apps are confined to the users home directory
                if (SystemEnvironment.isInkscapeInstalledAsSnap) {
                    setPreviewCodeInTemp(createTempDir(directory = File(System.getProperty("user.home"))))
                }
                else {
                    setPreviewCodeInTemp(createTempDir())
                }
            }
            catch (exception: AccessDeniedException) {
                previewForm.setLatexErrorMessage("${exception.message}")
            }
            catch (exception: IOException) {
                previewForm.setLatexErrorMessage("${exception.message}")
            }
        }
    }

    /**
     * Run inkscape command to convert pdf to png, depending on the version of inkscape.
     */
    private fun runInkscape(tempBasename: String, tempDirectory: File) {
        // If 1.0 or higher
        if (SystemEnvironment.inkscapeMajorVersion >= 1) {
            runCommand(
                inkscapeExecutable(),
                arrayOf(
                    "$tempBasename.pdf",
                    "--export-area-drawing",
                    "--export-dpi", "1000",
                    "--export-background", "#FFFFFF",
                    "--export-background-opacity", "1.0",
                    "--export-filename", "$tempBasename.png"
                ),
                tempDirectory
            ) ?: throw AccessDeniedException(tempDirectory)
        }
        else {
            runCommand(
                pdf2svgExecutable(),
                arrayOf(
                    "$tempBasename.pdf",
                    "$tempBasename.svg"
                ),
                tempDirectory
            ) ?: return

            runCommand(
                inkscapeExecutable(),
                arrayOf(
                    "$tempBasename.svg",
                    "--export-area-drawing",
                    "--export-dpi", "1000",
                    "--export-background", "#FFFFFF",
                    "--export-png", "$tempBasename.png"
                ),
                tempDirectory
            ) ?: throw AccessDeniedException(tempDirectory)
        }
    }

    private fun runCommand(command: String, args: Array<String>, workDirectory: File): String? {

        val executable = Runtime.getRuntime().exec(
            arrayOf(command) + args,
            null,
            workDirectory
        )

        val (stdout, stderr) = executable.inputStream.bufferedReader().use { stdout ->
            executable.errorStream.bufferedReader().use { stderr ->
                Pair(stdout.readText(), stderr.readText())
            }
        }

        executable.waitFor(waitTime, TimeUnit.SECONDS)

        if (executable.exitValue() != 0) {
            previewForm.setLatexErrorMessage("$command exited with ${executable.exitValue()}\n$stdout\n$stderr")
            return null
        }

        return stdout
    }

    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.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.util

import nl.hannahsten.texifyidea.lang.Argument
import nl.hannahsten.texifyidea.lang.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.*

/**
 * Get the lowercase token type (including `@`) of the BibTeX entry (e.g. `@article`).
 */
fun BibtexEntry.tokenType(): String? = type.text.toLowerCase()

/**
 * Get the token type (excluding `@`) of the BibTeX entry (e.g. `article`).
 */
fun BibtexEntry.tokenName(): String? = tokenType()?.substring(1)

/**
 * Get the identifier/label of the BibTeX entry (e.g. `someAuthor:23b`).
 */
fun BibtexEntry.identifier(): String? = firstChildOfType(BibtexId::class)?.text?.substringEnd(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(): BibtexKey? {
    val keys = keys()
    if (keys.isEmpty()) {
        return null
    }
    return keys.first()
}

/**
 * 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()?.toLowerCase() == "string"

/**
 * Checks if the entry is a @preamble.
 */
fun BibtexEntry.isPreamble() = tokenName()?.toLowerCase() == "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()?.toLowerCase()
        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

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())

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.lang.RequiredFileArgument
import nl.hannahsten.texifyidea.lang.RequiredPicturePathArgument
import nl.hannahsten.texifyidea.psi.LatexCommands

/**
 * Finds all defined commands within the project.
 *
 * @return The found commands.
 */
fun Project.findCommandDefinitions(): Collection<LatexCommands> {
    return LatexDefinitionIndex.getItems(this).filter {
        it.name in Magic.Command.commandDefinitions
    }
}

/**
 * Get all commands that include other files, including backslashes.
 */
fun getIncludeCommands(): Set<String> {
    return LatexRegularCommand.values()
            .filter { command -> command.arguments.any { it is RequiredFileArgument || it is RequiredPicturePathArgument } }
            .map { "\\" + it.command }
            .toSet()
}

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
        if (item != other.item) return false
        return true
    }

    override fun hashCode() = item?.hashCode() ?: 0
}

package nl.hannahsten.texifyidea.util

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.
 *
 * @param offset
 *              Where to insert the string.
 * @param string
 *              The string to insert.
 */
fun Editor.insertAndMove(offset: Int, string: String) {
    val document = this.document
    runWriteAction { document.insertString(offset, string) }
    caretModel.moveToOffset(caretModel.offset + string.length)
}

/**
 * @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

/**
 * 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 Magic.Command.environmentDefinitions
    }
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.TextRange
import java.util.regex.Pattern

/**
 * Creates a pair of two objects, analogous to [to].
 */
infix fun <T1, T2> T1.and(other: T2) = Pair(this, other)

/**
 * Prints the object in default string presentation to the console.
 */
fun Any.print() = print(this)

/**
 * Prints the object in default string presentation to the console including line feed.
 */
fun Any.println() = println(this)

/**
 * Prints `message: OBJECT` to the console.
 */
infix fun Any.debug(message: Any) = print("$message: $this")

/**
 * Prints `message: OBJECT` to the console including line feed.
 */
infix fun Any.debugln(message: Any) = println("$message: $this")

/**
 * Executes the given run write action.
 */
fun runWriteAction(writeAction: () -> Unit) {
    ApplicationManager.getApplication().runWriteAction(writeAction)
}

/**
 * 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 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 com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.index.LatexParameterLabeledEnvironmentsIndex
import nl.hannahsten.texifyidea.lang.CommandManager
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.commandsInFileSet

/*
 * Collections
 */

/**
 * 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 defined labels within a given file.
 *
 * @receiver The file to analyse the file set of.
 * @return The found label commands.
 */
fun PsiFile.findLabelsInFileSetAsCollection(): List<PsiElement> = sequenceOf(findLabelingCommandsInFileSetAsSequence(),
    LatexParameterLabeledEnvironmentsIndex.getItemsInFileSet(this).asSequence()).flatten().toList()

/*
 * Sequences
 */

/**
 * Finds all the defined latex 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.findLatexLabelStringsInFileSetAsSequence(): Sequence<String> {
    return findLatexLabelPsiElementsInFileSetAsSequence().map { it.extractLabelName() }
}

/**
 * All labels in this file.
 */
fun PsiFile.findLatexLabelPsiElementsInFileAsSequence(): Sequence<PsiElement> = sequenceOf(findLabelingCommandsInFileAsSequence(),
        LatexParameterLabeledEnvironmentsIndex.getItems(this).asSequence()).flatten()

/**
 * All labels in the fileset.
 */
fun PsiFile.findLatexLabelPsiElementsInFileSetAsSequence(): Sequence<PsiElement> = sequenceOf(findLabelingCommandsInFileSetAsSequence(),
        LatexParameterLabeledEnvironmentsIndex.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.findLabelingCommandsInFileSetAsSequence(): Sequence<LatexCommands> {
    return this.commandsInFileSet().asSequence().findLatexCommandsLabels(this.project)
}

/**
 * @see [findLabelingCommandsInFileSetAsSequence] but then only for commands in this file.
 */
fun PsiFile.findLabelingCommandsInFileAsSequence(): Sequence<LatexCommands> {
    return this.commandsInFile().asSequence().findLatexCommandsLabels(this.project)
}

/*
 * Bibtex
 */

/**
 * 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.
 */
private 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" }

/*
 * 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 = Magic.Command.getLabelDefinitionCommands(project)
    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 = Magic.Command.getLabelDefinitionCommands(project)
    return filterIsInstance<LatexCommands>().filter { commandNames.contains(it.name) }
}

/*
 * Project
 */

/**
 * 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 result = ArrayList<PsiElement>(commands)
    result.addAll(bibtexIds)
    result.addAll(environments)
    return result
}

/*
 * Other utils
 */

/**
 * Extracts the label name from the PsiElement given that the PsiElement represents a label.
 */
fun PsiElement.extractLabelName(): String {
    return when (this) {
        is BibtexEntry -> identifier() ?: ""
        is LatexCommands -> {
            // 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
            val prefix = info?.prefix ?: ""
            // Skip optional parameters for now (also below and in
            prefix + this.requiredParameter(position)
        }
        is LatexEnvironment -> this.label ?: ""
        else -> text
    }
}

/**
 * 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? {
    return when (this) {
        is BibtexEntry -> firstChildOfType(BibtexId::class)
        is LatexCommands -> {
            // 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)
        }
        else -> null
    }
}

/**
 * Finds all section marker commands (as defined in [Magic.Command.sectionMarkers]) in the project.
 *
 * @return A list containing all the section marker [LatexCommands].
 */
fun Project.findSectionMarkers() = LatexCommandsIndex.getItems(this).filter {
    it.commandToken.text in Magic.Command.sectionMarkers
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.vfs.LocalFileSystem
import nl.hannahsten.texifyidea.run.compiler.LatexCompiler
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import java.io.File

/**
 * Try to find a latexmkrc file (see the latexmk man page).
 */
object LatexmkRcFileFinder {
    private val isSystemLatexmkRcFilePresent: Boolean by lazy {
        // 1
        if (SystemInfo.isLinux) {
            listOf(
                    "/opt/local/share/latexmk/LatexMk",
                    "/usr/local/share/latexmk/LatexMk",
                    "/usr/local/lib/latexmk/LatexMk",
                    "/cygdrive/local/share/latexmk/LatexMk"
            ).forEach {
                if (LocalFileSystem.getInstance().findFileByPath(it) != null) return@lazy true
                if (LocalFileSystem.getInstance().findFileByPath(it.replace("LatexMk", "latexmkrc")) != null) return@lazy true
            }
        }
        else if (SystemInfo.isWindows) {
            listOf(
                    "C:\\latexmk\\LatexMk",
                    "C:\\latexmk\\latexmkrc"
            ).forEach {
                if (LocalFileSystem.getInstance().findFileByPath(it) != null) return@lazy true
            }
        }
        System.getenv("LATEXMKRCSYS")?.let {
            if (LocalFileSystem.getInstance().findFileByPath(it) != null) return@lazy true
        }

        // 2
        listOf(
                "${System.getenv("XDG_CONFIG_HOME")}/latexmk/latexmkrc",
                "${System.getenv("HOME")}/.latexmkrc",
                "${System.getenv("USERPROFILE")}/.latexmkrc",
                "${System.getenv("HOME")}/.config/.latexmkrc"
        ).forEach {
            if (LocalFileSystem.getInstance().findFileByPath(it) != null) return@lazy true
        }

        false
    }

    private fun isLocalLatexmkRcFilePresent(compilerArguments: String?, workingDir: String?): Boolean {
        // 3
        if (workingDir != null) {
            listOf(
                    workingDir + File.separator + "latexmkrc",
                    workingDir + File.separator + ".latexmkrc"
            ).forEach {
                if (LocalFileSystem.getInstance().findFileByPath(it) != null) return true
            }
        }

        // 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]
                if (LocalFileSystem.getInstance().findFileByPath(path) != null) return true
            }
        }

        return false
    }

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

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.diagnostic.Logger

/**
 * @author Hannah Schellekens
 */
object Log {

    private val logger: Logger by lazy { Logger.getInstance(Log::class.java) }

    /**
     * Sends a formatted info message to the IntelliJ logger.
     *
     * All messages start with the prefix `TEXIFY-IDEA - `.
     *
     * @param format
     *         How the log should be formatted, see also [String.format].
     * @param objects
     *         The objects to bind to the format.
     */
    @JvmStatic
    fun logf(format: String, vararg objects: Any?) = logger.info("TEXIFY-IDEA - " + format.format(*objects))
}

package nl.hannahsten.texifyidea.util

import com.intellij.codeInspection.LocalQuickFix
import com.intellij.openapi.project.Project
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.file.*
import nl.hannahsten.texifyidea.inspections.latex.LatexLineBreakInspection
import nl.hannahsten.texifyidea.lang.CommandManager
import nl.hannahsten.texifyidea.lang.LatexRegularCommand.*
import nl.hannahsten.texifyidea.lang.Package
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSFONTS
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSSYMB
import nl.hannahsten.texifyidea.lang.Package.Companion.BIBLATEX
import nl.hannahsten.texifyidea.lang.Package.Companion.GLOSSARIES
import nl.hannahsten.texifyidea.lang.Package.Companion.GLOSSARIESEXTRA
import nl.hannahsten.texifyidea.lang.Package.Companion.GRAPHICS
import nl.hannahsten.texifyidea.lang.Package.Companion.GRAPHICX
import nl.hannahsten.texifyidea.lang.Package.Companion.MATHTOOLS
import nl.hannahsten.texifyidea.lang.Package.Companion.NATBIB
import nl.hannahsten.texifyidea.lang.Package.Companion.XCOLOR
import org.intellij.lang.annotations.Language
import java.awt.Color
import java.util.regex.Pattern

typealias LatexPackage = Package
typealias RegexPattern = Pattern

/**
 * Magic constants are awesome!
 *
 * @author Hannah Schellekens
 */
object Magic {

    /**
     * @author Hannah Schellekens
     */
    object General {

        @JvmField
        val noQuickFix: LocalQuickFix? = null

        /**
         * Abbreviations not detected by [Pattern.abbreviation].
         */
        val unRegexableAbbreviations = listOf(
            "et al."
        )

        @JvmField
        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")
        @JvmField
        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()
    }

    /**
     * @author Hannah Schellekens
     */
    object Typography {

        /**
         * 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"
        )
    }

    /**
     * @author Hannah Schellekens
     */
    object Environment {

        @JvmField
        val listingEnvironments = hashSetOf("itemize", "enumerate", "description")

        @JvmField
        val tableEnvironments = hashSetOf("tabular", "tabular*", "tabularx", "array", "longtable")

        /**
         * Map that maps all environments that are expected to have a label to the label prefix they have by convention.
         *
         * environment name `=>` label prefix without colon
         */
        @JvmField
        val labeled = mapOfVarargs(
                "figure", "fig",
                "table", "tab",
                "equation", "eq",
                "algorithm", "alg",
                "lstlisting", "lst",
                "Verbatim", "verb"
        )

        /**
         * Environments that define their label via an optional parameter
         */
        @JvmField
        val labelAsParameter = hashSetOf("lstlisting", "Verbatim")

        /**
         * Environments that introduce figures
         */
        @JvmField
        val figures = hashSetOf("figure")

        @JvmField
        val verbatim = hashSetOf("verbatim", "Verbatim", "lstlisting", "plantuml", "luacode", "luacode*", "sagesilent", "sageblock", "sagecommandline", "sageverbatim", "sageexample")

        @JvmField
        val algorithmEnvironments = setOf("algorithmic")

        /**
         * All environments that define a matrix.
         */
        @JvmField
        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"
        )

        @JvmField
        val alignableEnvironments = setOf(
                "eqnarray", "eqnarray*",
                "split",
                "align", "align*",
                "alignat", "alignat*",
                "flalign", "flalign*",
                "aligned", "alignedat",
                "cases", "dcases"
        ) + matrixEnvironments
    }

    /**
     * @author Abby Berkers
     */
    object Comment {

        @JvmField
        val preambleValues = hashSetOf("tikz", "math")
    }

    /**
     * @author Hannah Schellekens
     */
    object Command {

        /**
         * LaTeX commands that make the text take up more vertical space.
         */
        @JvmField
        val high = hashSetOf(
                "\\frac", "\\dfrac", "\\sqrt", "\\sum", "\\int", "\\iint", "\\iiint", "\\iiiint",
                "\\prod", "\\bigcup", "\\bigcap", "\\bigsqcup", "\\bigsqcap"
        )

        /**
         * Maps commands to their expected label prefix. Which commands are expected to have a label at all is determined in settings.
         */
        @JvmField
        val labeledPrefixes = mapOf(
                "\\" + CHAPTER.command to "ch",
                "\\" + SECTION.command to "sec",
                "\\" + SUBSECTION.command to "subsec",
                "\\" + SUBSUBSECTION.command to "subsubsec",
                "\\" + ITEM.command to "itm"
        )

        /**
         * Level of labeled commands.
         */
        val labeledLevels = 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
        )

        /**
         * All commands that mark some kind of section.
         */
        @JvmField
        val sectionMarkers = listOf(
            PART, CHAPTER, SECTION, SUBSECTION, SUBSUBSECTION, PARAGRAPH, SUBPARAGRAPH
        ).map { "\\" + it.command }

        /**
         * The colours that each section separator has.
         */
        @JvmField
        val sectionSeparatorColors = mapOf(
            "\\${PART.command}" to Color(152, 152, 152),
            "\\${CHAPTER.command}" to Color(172, 172, 172),
            "\\${SECTION.command}" to Color(182, 182, 182),
            "\\${SUBSECTION.command}" to Color(202, 202, 202),
            "\\${SUBSUBSECTION.command}" to Color(212, 212, 212),
            "\\${PARAGRAPH.command}" to Color(222, 222, 222),
            "\\${SUBPARAGRAPH.command}" to Color(232, 232, 232)
        )

        /**
         * LaTeX commands that increase a counter that can be labeled.
         */
        @JvmField
        val increasesCounter = hashSetOf("\\caption", "\\captionof") + labeledPrefixes.keys

        /**
         * All commands that represent a reference to a label, excluding user defined commands.
         */
        @JvmField
        val labelReferenceWithoutCustomCommands = hashSetOf(
                "\\ref", "\\eqref", "\\nameref", "\\autoref",
                "\\fullref", "\\pageref", "\\vref", "\\Autoref", "\\cref", "\\Cref",
                "\\labelcref", "\\cpageref"
        )

        /**
         * All commands that represent a reference to a label, including user defined commands.
         */
        fun getLabelReferenceCommands(project: Project): Set<String> {
            CommandManager.updateAliases(labelReferenceWithoutCustomCommands, project)
            return CommandManager.getAliases(labelReferenceWithoutCustomCommands.first())
        }

        /**
         * All commands that represent a reference to a bibliography entry/item.
         * Commands listed here should also be listed in [nl.hannahsten.texifyidea.lang.LatexRegularCommand].
         */
        @JvmField
        val bibliographyReference = hashSetOf(
                "\\cite", "\\nocite", "\\citep", "\\citep*", "\\citet", "\\citet*", "\\Citep",
                "\\Citep*", "\\Citet", "\\Citet*", "\\citealp", "\\citealp*", "\\citealt", "\\citealt*",
                "\\Citealp", "\\Citealp*", "\\Citealt", "\\Citealt*", "\\citeauthor", "\\citeauthor*",
                "\\Citeauthor", "\\Citeauthor*", "\\citeyear", "\\citeyearpar", "\\parencite", "\\Parencite",
                "\\footcite", "\\footcitetext", "\\textcite", "\\Textcite", "\\smartcite", "\\Smartcite",
                "\\cite*", "\\parencite*", "\\supercite", "\\autocite", "\\Autocite", "\\autocite*",
                "\\Autocite*", "\\citetitle", "\\citetitle*", "\\citeyear*", "\\citedate", "\\citedate*",
                "\\citeurl", "\\volcite", "\\Volcite", "\\pvolcite", "\\Pvolcite", "\\fvolcite",
                "\\Fvolcite", "\\ftvolcite", "\\svolcite", "\\Svolcite", "\\tvolcite", "\\Tvolcite",
                "\\avolcite", "\\Avolcite", "\\fullcite", "\\footfullcite", "\\notecite", "\\Notecite",
                "\\pnotecite", "\\fnotecite"
        )

        /**
         * All commands that represent some kind of reference (think \ref and \cite).
         */
        @JvmField
        val reference = labelReferenceWithoutCustomCommands + bibliographyReference

        /**
         * Commands from the import package which require an absolute path as first parameter.
         */
        val absoluteImportCommands = setOf("\\includefrom", "\\inputfrom", "\\import")

        /**
         * Commands from the import package which require a relative path as first parameter.
         */
        val relativeImportCommands = setOf("\\subimport", "\\subinputfrom", "\\subincludefrom")

        /**
         * All commands for which we assume that commas in required parameters do not separate parameters.
         * By default we assume the comma is a separator.
         */
        val commandsWithNoCommaSeparatedParameters = setOf(INCLUDEGRAPHICS).map { "\\" + it.command }

        /**
         * All commands that define labels and that are present by default.
         * To include user defined commands, use [getLabelDefinitionCommands] (may be significantly slower).
         */
        @JvmField
        val labelDefinitionsWithoutCustomCommands = setOf("\\label")

        /**
         * 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.
         */
        @JvmField
        val bibliographyItems = setOf("\\" + BIBITEM.command)

        /**
         * All math operators without a leading slash.
         */
        @JvmField
        val slashlessMathOperators = hashSetOf(
                "arccos", "arcsin", "arctan", "arg", "cos", "cosh", "cot", "coth", "csc",
                "deg", "det", "dim", "exp", "gcd", "hom", "inf", "ker", "lg", "lim", "liminf", "limsup",
                "ln", "log", "max", "min", "Pr", "sec", "sin", "sinh", "sup", "tan", "tanh"
        )

        /**
         * All commands that define regular commands, and that require that the command is not already defined.
         */
        val regularStrictCommandDefinitions = hashSetOf(
                "\\" + NEWCOMMAND.command,
                "\\" + NEWCOMMAND_STAR.command,
                "\\" + NEWIF.command,
                "\\" + NEWDOCUMENTCOMMAND.command
        )

        /**
         * All commands that define or redefine other commands, whether it exists or not.
         */
        @JvmField
        val redefinitions = hashSetOf(
                RENEWCOMMAND,
                RENEWCOMMAND_STAR,
                PROVIDECOMMAND, // Does nothing if command exists
                PROVIDECOMMAND_STAR,
                PROVIDEDOCUMENTCOMMAND, // Does nothing if command exists
                DECLAREDOCUMENTCOMMAND,
                DEF,
                LET,
                RENEWENVIRONMENT
        ).map { "\\" + it.command }

        /**
         * All commands that define or redefine regular commands.
         */
        val regularCommandDefinitions = regularStrictCommandDefinitions + redefinitions

        /**
         * All commands that define commands that should be used exclusively
         * in math mode.
         */
        @JvmField
        val mathCommandDefinitions = hashSetOf(
                "\\" + DECLARE_MATH_OPERATOR.command,
                "\\" + DECLARE_PAIRED_DELIMITER.command,
                "\\" + DECLARE_PAIRED_DELIMITER_X.command,
                "\\" + DECLARE_PAIRED_DELIMITER_XPP.command
        )

        /**
         * All commands that define new commands.
         */
        @JvmField
        val commandDefinitions = regularCommandDefinitions + mathCommandDefinitions

        /**
         * All commands that define new documentclasses.
         */
        @JvmField
        val classDefinitions = hashSetOf("\\" + PROVIDESCLASS.command)

        /**
         * All commands that define new packages.
         */
        @JvmField
        val packageDefinitions = hashSetOf("\\" + PROVIDESPACKAGE.command)

        /**
         * All commands that define new environments.
         */
        @JvmField
        val environmentDefinitions = hashSetOf(
            NEWENVIRONMENT,
            NEWTHEOREM,
            NEWDOCUMENTENVIRONMENT,
            PROVIDEDOCUMENTENVIRONMENT,
            DECLAREDOCUMENTENVIRONMENT
        ).map { "\\" + it.command }

        /**
         * All commands that define stuff like classes, environments, and definitions.
         */
        @JvmField
        val definitions = commandDefinitions + classDefinitions + packageDefinitions + environmentDefinitions

        /**
         * Commands for which TeXiFy-IDEA has essential custom behaviour and which should not be redefined.
         */
        @JvmField
        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.
         */
        @JvmField
        val illegalExtensions = mapOf(
                "\\" + INCLUDE.command to listOf(".tex"),
                "\\" + SUBFILEINCLUDE.command to listOf(".tex"),
                "\\" + BIBLIOGRAPHY.command to listOf(".bib")
        )

        /**
         * Commands that should have the given file extensions.
         */
        @JvmField
        val requiredExtensions = mapOf(
                "\\" + ADDBIBRESOURCE.command to listOf("bib")
        )

        /**
         * Extensions that should only be scanned for the provided include commands.
         */
        @JvmField
        val includeOnlyExtensions: Map<String, HashSet<String>> = mapOf(
                "\\include" to hashSetOf("tex"),
                "\\includeonly" to hashSetOf("tex"),
                "\\subfile" to hashSetOf("tex"),
                "\\subfileinclude" to hashSetOf("tex"),
                "\\bibliography" to hashSetOf("bib"),
                "\\addbibresource" to hashSetOf("bib"),
                "\\RequirePackage" to hashSetOf("sty"),
                "\\usepackage" to hashSetOf("sty"),
                "\\documentclass" to hashSetOf("cls"),
                "\\" + EXTERNALDOCUMENT.command to hashSetOf("tex") // Not completely true, as it only includes labels
        )

        val includeCommands = includeOnlyExtensions.keys

        /**
         * All commands that end if.
         */
        @JvmField
        val endIfs = hashSetOf("\\fi")

        /**
         * All commands that at first glance look like \if-esque commands, but that actually aren't.
         */
        @JvmField
        val ignoredIfs = hashSetOf("\\newif", "\\iff", "\\ifthenelse", "\\iftoggle", "\\ifoot", "\\ifcsvstrcmp")

        /**
         * List of all TeX style primitives.
         */
        @JvmField
        val stylePrimitives = listOf(
                "\\rm", "\\sf", "\\tt", "\\it", "\\sl", "\\sc", "\\bf"
        )

        /**
         * The LaTeX counterparts of all [stylePrimitives] commands where %s is the content.
         */
        @JvmField
        val stylePrimitveReplacements = listOf(
                "\\textrm{%s}", "\\textsf{%s}", "\\texttt{%s}", "\\textit{%s}",
                "\\textsl{%s}", "\\textsc{%s}", "\\textbf{%s}"
        )

        /**
         * Set of text styling commands
         */
        @JvmField
        val textStyles = setOf(
                "\\textrm", "\\textsf", "\\texttt", "\\textit",
                "\\textsl", "\\textsc", "\\textbf", "\\emph",
                "\\textup", "\\textmd"
        )

        /**
         * All LaTeX commands that contain a url (in their first parameter).
         */
        @JvmField
        val urls = hashSetOf("\\url", "\\href")

        /**
         * All BibTeX tags that take a url as their parameter.
         */
        @JvmField
        val bibUrls = hashSetOf("url", "biburl")
    }

    /**
     * @author Hannah Schellekens
     */
    object Pattern {

        @JvmField
        val ellipsis = Regex("""(?<!\.)(\.\.\.)(?!\.)""")

        /**
         * This is the only correct way of using en dashes.
         */
        @JvmField
        val correctEnDash = RegexPattern.compile("[0-9]+--[0-9]+")!!

        /**
         * Matches the prefix of a label. Amazing comment this is right?
         */
        @JvmField
        val labelPrefix = RegexPattern.compile(".*:")!!

        /**
         * Matches the end of a sentence.
         *
         * Includes `[^.][^.]` because of abbreviations (at least in Dutch) like `s.v.p.`
         */
        @JvmField
        val sentenceEnd = RegexPattern.compile("([^.A-Z][^.A-Z][.?!;;] +[^%\\s])|(^\\. )")!!

        /**
         * Matches all interpunction that marks the end of a sentence.
         */
        @JvmField
        val sentenceSeparator = RegexPattern.compile("[.?!;;]")!!

        /**
         * Matches all sentenceSeparators at the end of a line (with or without space).
         */
        @JvmField
        val sentenceSeparatorAtLineEnd = RegexPattern.compile("$sentenceSeparator\\s*$")!!

        /**
         * Matches when a string ends with a non breaking space.
         */
        @JvmField
        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].
         */
        @JvmField
        val abbreviation = RegexPattern.compile("[0-9A-Za-z.]+\\.[A-Za-z](\\.[\\s~])")!!

        /** [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 everything except comments which start with % and end with a newline.
         */
        @JvmField
        val noComments: RegexPattern = RegexPattern.compile("(?<=^|\\n)[^%]+")

        /**
         * Matches leading and trailing whitespace on a string.
         */
        @JvmField
        val excessWhitespace = RegexPattern.compile("(^(\\s+).*(\\s*)\$)|(^(\\s*).*(\\s+)\$)")!!

        /**
         * Matches a non-ASCII character.
         */
        @JvmField
        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.
         */
        @JvmField
        val parameterSplit = RegexPattern.compile("\\s*,\\s*")!!

        /**
         * Matches the extension of a file name.
         */
        @JvmField
        val fileExtension = RegexPattern.compile("\\.[a-zA-Z0-9]+$")!!

        /**
         * Matches all leading whitespace.
         */
        @JvmField
        val leadingWhitespace = RegexPattern.compile("^\\s*")!!

        /**
         * Matches newlines.
         */
        @JvmField
        val newline = RegexPattern.compile("\\n")!!

        /**
         * Checks if the string is `text`, two newlines, `text`.
         */
        @JvmField
        val containsMultipleNewlines = RegexPattern.compile("[^\\n]*\\n\\n+[^\\n]*")!!

        /**
         * Matches HTML tags of the form `<tag>`, `<tag/>` and `</tag>`.
         */
        @JvmField
        val htmlTag = RegexPattern.compile("""<.*?/?>""")!!

        /**
         * Matches a LaTeX command that is the start of an \if-\fi structure.
         */
        @JvmField
        val ifCommand = RegexPattern.compile("\\\\if[a-zA-Z@]*")!!

        /**
         * Matches the begin and end commands of the cases and split environments.
         */
        @JvmField
        val casesOrSplitCommands = Regex("((?=\\\\begin\\{cases})|(?<=\\\\begin\\{cases}))" +
                "|((?=\\\\end\\{cases})|(?<=\\\\end\\{cases}))" +
                "|((?=\\\\begin\\{split})|(?<=\\\\begin\\{split}))" +
                "|((?=\\\\end\\{split})|(?<=\\\\end\\{split}))")
    }

    /**
     * @author Hannah Schellekens
     */
    object File {

        /**
         * All file extensions of files that can be included (and where the included files contain language that needs to be considered).
         */
        @JvmField
        val includeExtensions = hashSetOf("tex", "sty", "cls", "bib")

        val automaticExtensions = mapOf(
                "\\include" to LatexFileType.defaultExtension,
                "\\bibliography" to BibtexFileType.defaultExtension
        )

        /**
         * All possible file types.
         */
        @JvmField
        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).
         */
        @JvmField
        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")
    }

    /**
     * @author Hannah Schellekens
     */
    object Package {

        /**
         * All unicode enabling packages.
         */
        @JvmField
        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(GLOSSARIES, GLOSSARIESEXTRA).map { it.name }

        /**
         * Known conflicting packages.
         */
        val conflictingPackages = listOf(
                setOf(BIBLATEX, NATBIB)
        )

        /**
         * Maps packages to the packages it loads.
         */
        val packagesLoadingOtherPackages = mapOf(
                AMSSYMB to setOf(AMSFONTS),
                MATHTOOLS to setOf(AMSMATH),
                GRAPHICX to setOf(GRAPHICS),
                XCOLOR to setOf(COLOR)
        )

        /**
         * 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
        )
    }

    /**
     * @author Hannah Schellekens
     */
    object Icon {

        /**
         * Maps file extentions to their corresponding icons.
         */
        @JvmField
        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
        )
    }

    object Colors {
        /**
         * All commands that have a color as an argument.
         */
        @JvmField
        val takeColorCommands = values()
                .filter {
                    it.arguments.map { it.name }.contains("color")
                }
                .map { it.command }

        /**
         * All commands that define a new color.
         */
        @JvmField
        val colorDefinitions = values()
                .filter { it.dependency == XCOLOR }
                .filter { it.arguments.map { it.name }.contains("name") }

        @JvmField
        val colorCommands = takeColorCommands + colorDefinitions.map { it.command }

        @JvmField
        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
        )
    }
}

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)

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.progress.Task
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.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.lang.Package
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.files.*

/**
 * @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()

    /** Commands which can include packages in optional or required arguments. **/
    val PACKAGE_COMMANDS = setOf("\\usepackage", "\\RequirePackage", "\\documentclass")
    private val TIKZ_IMPORT_COMMANDS = setOf("\\usetikzlibrary")
    private val PGF_IMPORT_COMMANDS = setOf("\\usepgfplotslibrary")

    /**
     * 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 (!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.requiredParameters.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

        // https://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/modifying_psi.html?search=refac#combining-psi-and-document-modifications
        // Avoid 'Write access is allowed inside write-action only" exception
        runWriteAction {
            // Avoid "Attempt to modify PSI for non-committed Document"
            PsiDocumentManager.getInstance(file.project).doPostponedOperationsAndUnblockDocument(file.document() ?: return@runWriteAction)
            PsiDocumentManager.getInstance(file.project).commitDocument(file.document() ?: return@runWriteAction)
            if (anchorAfter != null) {
                val anchorBefore = anchorAfter.node.treeNext
                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.
     */
    @JvmStatic
    fun insertUsepackage(file: PsiFile, pack: Package): Boolean {
        if (pack.isDefault) {
            return true
        }

        if (file.includedPackages().contains(pack.name)) {
            return true
        }

        // Don't insert when a conflicting package is already present
        if (Magic.Package.conflictingPackages.any { it.contains(pack) }) {
            for (conflicts in Magic.Package.conflictingPackages) {
                // Assuming the package is not already included
                if (conflicts.contains(pack) && file.includedPackages().toSet()
                                .intersect(conflicts.map { it.name })
                                .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 used packages in the included file set.
     *
     * @return A set containing all used package names.
     */
    @JvmStatic
    fun getIncludedPackages(baseFile: PsiFile): Set<String> {
        val commands = baseFile.commandsInFileSet()
        return getIncludedPackages(commands, HashSet())
    }

    /**
     * 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())
    }

    /**
     * Analyses the given file and finds all the used packages in the included file set.
     *
     * @return A list containing all used package names (including duplicates).
     */
    @JvmStatic
    fun getIncludedPackagesList(baseFile: PsiFile): List<String> {
        val commands = baseFile.commandsInFileSet()
        return getIncludedPackages(commands, ArrayList())
    }

    /**
     * Gets all packages imported with [PACKAGE_COMMANDS].
     */
    @JvmStatic
    fun <T : MutableCollection<String>> getIncludedPackages(
            commands: Collection<LatexCommands>,
            result: T
    ) = getPackagesFromCommands(commands, PACKAGE_COMMANDS, result)

    /**
     * Gets all packages imported with [TIKZ_IMPORT_COMMANDS].
     */
    @JvmStatic
    fun <T : MutableCollection<String>> getIncludedTikzLibraries(
            commands: Collection<LatexCommands>,
            result: T
    ) = getPackagesFromCommands(commands, TIKZ_IMPORT_COMMANDS, result)

    /**
     * Gets all packages imported with [PGF_IMPORT_COMMANDS].
     */
    @JvmStatic
    fun <T : MutableCollection<String>> getIncludedPgfLibraries(
            commands: Collection<LatexCommands>,
            result: T
    ) = getPackagesFromCommands(commands, PGF_IMPORT_COMMANDS, result)

    /**
     * Analyses all the given commands and reduces it to a set of all included packages.
     * Classes will not be included.
     *
     * Note that not all elements returned may be valid package names.
     */
    private fun <T : MutableCollection<String>> getPackagesFromCommands(
            commands: Collection<LatexCommands>,
            packageCommands: Set<String>,
            initial: T
    ): T {
        for (cmd in commands) {
            if (cmd.commandToken.text !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 == "\\" + LatexRegularCommand.ONLYIFSTANDALONE.command) {
                continue
            }

            // Assume packages can be included in both optional and required parameters
            // Except a class, because a class is not a package
            val packages = if (cmd.commandToken
                            .text == "\\documentclass" || cmd.commandToken
                            .text == "\\LoadClass") {
                setOf(cmd.optionalParameters.keys.toList())
            }
            else {
                setOf(cmd.requiredParameters, cmd.optionalParameters.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
    }
}

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): String? {
        // Find the package name for tlmgr.
        task.title = "Searching for $packageName..."
        // 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 = "tlmgr search --file --global /$packageName.sty".runCommand()
                ?: return null

        // Check if tlmgr needs to be updated first, and do so if needed.
        val tlmgrUpdateCommand = "tlmgr 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\.  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
        }
    }
}

/**
 * @see PackageUtils.insertUsepackage
 */
fun PsiFile.insertUsepackage(pack: Package) = PackageUtils.insertUsepackage(this, pack)

/**
 * @see PackageUtils.getIncludedPackages
 */
fun PsiFile.includedPackages(): Collection<String> = PackageUtils.getIncludedPackages(this)

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.fileTypes.FileType
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 nl.hannahsten.texifyidea.index.LatexDefinitionIndex
import nl.hannahsten.texifyidea.util.files.allChildFiles

/**
 * 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.requiredParameters }
            .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> {
    val scope = GlobalSearchScope.projectScope(this)
    return FileTypeIndex.getFiles(type, scope)
}

package nl.hannahsten.texifyidea.util

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 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.files.commandsInFileSet
import kotlin.reflect.KClass

// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// // PSI ELEMENT ///////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * 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) return emptyList()
    return 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? {
    for (child in this.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 {
    return 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
}

/**
 * @see LatexPsiUtil.getPreviousSiblingIgnoreWhitespace
 */
fun PsiElement.previousSiblingIgnoreWhitespace() = LatexPsiUtil.getPreviousSiblingIgnoreWhitespace(this)

/**
 * @see LatexPsiUtil.getNextSiblingIgnoreWhitespace
 */
fun PsiElement.nextSiblingIgnoreWhitespace() = LatexPsiUtil.getNextSiblingIgnoreWhitespace(this)

/**
 * Finds the next sibling of the element that has the given type.
 *
 * @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? {
    var sibling: PsiElement? = this
    while (sibling != null) {
        if (clazz.java.isAssignableFrom(sibling::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return sibling as T
        }

        sibling = sibling.nextSibling
    }

    return null
}

/**
 * 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? {
    var sibling: PsiElement? = this
    while (sibling != null) {
        if (clazz.java.isAssignableFrom(sibling::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return sibling as T
        }

        sibling = sibling.prevSibling
    }

    return null
}

/**
 * @see LatexPsiUtil.getAllChildren
 */
fun PsiElement.allChildren(): List<PsiElement> = LatexPsiUtil.getAllChildren(this)

/**
 * @see LatexPsiUtil.getChildren
 */
fun PsiElement.allLatexChildren(): List<PsiElement> = LatexPsiUtil.getChildren(this)

/**
 * 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)
}

/**
 * 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.
 */
inline fun PsiElement.forEachChild(action: (PsiElement) -> Unit) {
    for (child in children) action(child)
}

/**
 * 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.isMagicComment(): Boolean = TextBasedMagicCommentParser.COMMENT_PREFIX.containsMatchIn(text)

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

// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// // LATEX ELEMENTS ////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * 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) = 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 LatexContent.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" }

/**
 * Splits the first normal text child element of [element] on [delimiter].
 *
 * Note that only the first [LatexParameterText] child content is processed.
 * When other PSI children are present in the parameter, these are ignored.
 *
 * @return The split contents.
 */
fun splitContent(element: PsiElement, delimiter: String = ",") = element.firstChildOfType(LatexParameterText::class)?.text?.split(delimiter) ?: emptyList()

/**
 * Splits the plain text contents on [delimiter].
 *
 * Note that only the first [LatexParameterText] child content is processed.
 * When other PSI children are present in the parameter, these are ignored.
 *
 * @return The split contents.
 */
fun LatexRequiredParam.splitContent(delimiter: String = ",") = splitContent(this, delimiter)

/**
 * Splits the plain text contents on [delimiter].
 *
 * Note that only the first [LatexParameterText] child content is processed.
 * When other PSI children are present in the parameter, these are ignored.
 *
 * @return The split contents.
 */
fun LatexOptionalParam.splitContent(delimiter: String = ",") = splitContent(this, delimiter)

/**
 * Splits the plain text contents on [delimiter].
 *
 * Note that only the first [LatexParameterText] child content is processed.
 * When other PSI children are present in the parameter, these are ignored.
 *
 * @return The split contents.
 */
fun LatexParameter.splitContent(delimiter: String = ",") = splitContent(this, delimiter)

package nl.hannahsten.texifyidea.util

import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.util.PsiTreeUtil
import nl.hannahsten.texifyidea.lang.LatexMathCommand
import nl.hannahsten.texifyidea.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.lang.RequiredArgument
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.files.document
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 Magic.Command.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 Magic.Colors.colorDefinitions.map { it.command }

fun LatexCommands?.usesColor() = this != null && this.name?.substring(1) in Magic.Colors.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 Magic.Command.redefinitions || this.name in Magic.Command.redefinitions)

/**
 * 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 Magic.Command.regularCommandDefinitions || name in Magic.Command.mathCommandDefinitions)

/**
 * 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 {
    return this != null && ("\\newenvironment" == name ||
            "\\renewenvironment" == name)
}

/**
 * Get the command that gets defined by a definition (`\let` or `\def` command).
 */
fun LatexCommands.definitionCommand(): LatexCommands? = forcedFirstRequiredParameterAsCommand()

/**
 * 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(LatexContent::class) ?: return null
    val next = content.nextSiblingIgnoreWhitespace() as? LatexContent
            ?: 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(LatexContent::class) ?: return null
    val previous = content.previousSiblingIgnoreWhitespace() as? LatexContent
            ?: return null
    return previous.firstChildOfType(LatexCommands::class)
}

/**
 * Get the name of the command that is defined by `this` command.
 */
fun LatexCommands.definedCommandName() = when (name) {
    in Magic.Command.mathCommandDefinitions + setOf("\\newcommand") -> forcedFirstRequiredParameterAsCommand()?.name
    else -> definitionCommand()?.name
}

/**
 * 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 requiredParameters[min(requiredArgIndices.first(), requiredParameters.size - 1)]
}

/**
 * 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 = requiredParameters
    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
    }

    val parent = PsiTreeUtil.getParentOfType(this, LatexContent::class.java)
    val sibling = PsiTreeUtil.getNextSiblingOfType(parent, LatexContent::class.java)
    return PsiTreeUtil.findChildOfType(sibling, LatexCommands::class.java)
}

/**
 * Get all [LatexCommands] that are children of the given element.
 */
fun PsiElement.allCommands(): List<LatexCommands> {
    val commands = ArrayList<LatexCommands>()
    allCommands(commands)
    return commands
}

/**
 * Recursive implementation of [allCommands].
 */
private fun PsiElement.allCommands(commands: MutableList<LatexCommands>) {
    forEachChild { it.allCommands(commands) }
    if (this is LatexCommands) {
        commands.add(this)
    }
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.util.TextRange
import org.intellij.lang.annotations.Language
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.math.min

/**
 * Capitalises the first character of the string.
 */
fun String.capitalizeFirst(): String = this[0].toUpperCase() + substring(1, length)

/**
 * Converts the string to camel case.
 */
fun String.camelCase(): String {
    @Language("RegExp")
    val parts = toLowerCase().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)
    }
}

/**
 * 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 = Magic.Pattern.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.toLowerCase()}"
    val thisLower = toLowerCase()

    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.isBlank() }
            .map { Pair(it.getIndent().length, it) }
            .minBy { 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() = Magic.Pattern.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, 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
            .toLowerCase()

    // If there are no valid characters left, use a default name.
    return if (formatted.isEmpty()) "myfile" else formatted
}

/**
 * Formats the string as a valid LaTeX label name.
 */
fun String.formatAsLabel(): String {
    return replace(" ", "-")
            .removeAll("%", "~", "#", "\\")
            .toLowerCase()
}

/**
 * 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 [Magic.Pattern.htmlTag]
 */
fun String.removeHtmlTags() = this.replace(Magic.Pattern.htmlTag.toRegex(), "")

/**
 * Run a command in the terminal.
 *
 * @return The output of the command or null if an exception was thrown.
 */
fun String.runCommand(): String? {
    return try {
        val parts = this.split("\\s".toRegex())
        val proc = ProcessBuilder(*parts.toTypedArray())
                .redirectOutput(ProcessBuilder.Redirect.PIPE)
                .redirectError(ProcessBuilder.Redirect.PIPE)
                .start()

        // Timeout value
        proc.waitFor(10, TimeUnit.SECONDS)
        proc.inputStream.bufferedReader().readText().trim() + proc.errorStream.bufferedReader().readText().trim()
    }
    catch (e: IOException) {
        null
    }
}

/**
 * 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) }

/** 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))

package nl.hannahsten.texifyidea.util

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
        }

        val isTlmgrInstalled: Boolean by lazy {
            "tlmgr --version".runCommand()?.contains("TeX Live") == true
        }

        val isPerlInstalled: Boolean by lazy {
            "perl -v".runCommand()?.contains("This is perl") == true
        }
    }
}

fun runCommand(vararg commands: String): String {
    try {
        val command = arrayListOf(*commands)
        val proc = ProcessBuilder(command)
                .redirectOutput(ProcessBuilder.Redirect.PIPE)
                .redirectError(ProcessBuilder.Redirect.PIPE)
                .start()

        // Timeout value
        proc.waitFor(3, TimeUnit.SECONDS)
        return proc.inputStream.bufferedReader().readText()
    }
    catch (e: IOException) {
        e.printStackTrace()
    }
    return ""
}

package nl.hannahsten.texifyidea.util

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.MessageType
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.wm.WindowManager
import com.intellij.ui.awt.RelativePoint

/**
 * Shows a toast message above the loading bar.
 *
 * @param project
 *          The current project.
 * @param type
 *          The type of toast to toast.
 * @param htmlMessage
 *          What to show in the toast. Supports HTML.
 */
fun toast(project: Project, type: MessageType, htmlMessage: String) {
    val statusBar = WindowManager.getInstance().getStatusBar(project)
    JBPopupFactory.getInstance()
            .createHtmlTextBalloonBuilder(htmlMessage, type, null)
            .setFadeoutTime(7500)
            .createBalloon()
            .show(RelativePoint.getCenterOf(statusBar.component), Balloon.Position.above)
}

/**
 * Shows an information toast message above the loading bar.
 *
 * @param project
 *          The current project.
 * @param htmlMessage
 *          What to show in the toast. Supports HTML.
 */
fun toastInfo(project: Project, htmlMessage: String) = toast(project, MessageType.INFO, htmlMessage)

/**
 * Shows an error toast message above the loading bar.
 *
 * @param project
 *          The current project.
 * @param htmlMessage
 *          What to show in the toast. Supports HTML.
 */
fun toastError(project: Project, htmlMessage: String) = toast(project, MessageType.ERROR, htmlMessage)

/**
 * Shows a warning information toast message above the loading bar.
 *
 * @param project
 *          The current project.
 * @param htmlMessage
 *          What to show in the toast. Supports HTML.
 */
fun toastWarning(project: Project, htmlMessage: String) = toast(project, MessageType.WARNING, htmlMessage)

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.*
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.files.referencedFileSet
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

        val (words, chars) = countWords(psiFile)
        val dialog = makeDialog(psiFile, words, chars)

        dialog.show()
    }

    /**
     * Builds the dialog that must show the word count.
     */
    private fun makeDialog(baseFile: PsiFile, wordCount: Int, characters: Int): DialogBuilder {
        return DialogBuilder().apply {
            setTitle("Word count")

            setCenterPanel(JLabel(
                    """|
                        |
Analysis of ${baseFile.name} (and inclusions):

                        |

                        |
Word count:	$wordCount

                        |
Character count:	$characters
                        |

                        |""".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 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 (wordsBib, charsBib) = countWords(bibliography)

        return Pair(wordsNormal - wordsBib, charsNormal - 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<LatexNormalText>): 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.toLowerCase() == "s") {
                if (split.size == 1) return 1

                if (i > 0 && CONTRACTION_S.contains(split[i - 1].toLowerCase())) {
                    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

        if (IGNORE_COMMANDS.contains(command.name)) {
            return true
        }

        return false
    }
}

package nl.hannahsten.texifyidea.action.evince

import nl.hannahsten.texifyidea.action.ForwardSearchActionBase
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

/**
 * Starts a forward search action in Evince.
 *
 * Note: this is only available on Linux.
 *
 * @author Thomas Schouten
 */
open class ForwardSearchAction : ForwardSearchActionBase(PdfViewer.EVINCE)

package nl.hannahsten.texifyidea.action.group

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

class EvinceActionGroup : DefaultActionGroup() {

    override fun canBePerformed(context: DataContext) = PdfViewer.EVINCE.isAvailable()

    override fun hideIfNoVisibleChildren(): Boolean = true
}

package nl.hannahsten.texifyidea.action.group

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

package nl.hannahsten.texifyidea.action.group

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

package nl.hannahsten.texifyidea.action.group

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 LatexAnalyzeMenuGroup : DefaultActionGroup() {

    override fun update(event: AnActionEvent) {
        val file = event.getData(CommonDataKeys.PSI_FILE)
        event.presentation.isEnabledAndVisible = file?.isLatexFile() ?: false
    }
}

package nl.hannahsten.texifyidea.action.group

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

class OkularActionGroup : DefaultActionGroup() {

    override fun canBePerformed(context: DataContext) = PdfViewer.OKULAR.isAvailable()

    override fun hideIfNoVisibleChildren(): Boolean = true
}

package nl.hannahsten.texifyidea.action.group

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

class SkimActionGroup : DefaultActionGroup() {

    override fun canBePerformed(context: DataContext) = PdfViewer.SKIM.isAvailable()

    override fun hideIfNoVisibleChildren(): Boolean = true
}

package nl.hannahsten.texifyidea.action.group

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.hannahsten.texifyidea.run.sumatra.isSumatraAvailable

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class SumatraActionGroup : DefaultActionGroup() {

    override fun canBePerformed(context: DataContext) = isSumatraAvailable

    override fun hideIfNoVisibleChildren() = true
}

package nl.hannahsten.texifyidea.action.group

import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DefaultActionGroup
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

class ZathuraActionGroup : DefaultActionGroup() {
    override fun canBePerformed(context: DataContext) = PdfViewer.ZATHURA.isAvailable()

    override fun hideIfNoVisibleChildren(): Boolean = true
}

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 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.Package.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) {
            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.okular

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.ui.OkularConfigureInverseSearchDialog

/**
 * @author Abby Berkers
 */
class ConfigureInverseSearchAction : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
        OkularConfigureInverseSearchDialog()
    }

    /**
     * Hide this option when Okular is not available.
     */
    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = TexifySettings.getInstance().pdfViewer == PdfViewer.OKULAR
    }
}

package nl.hannahsten.texifyidea.action.okular

import nl.hannahsten.texifyidea.action.ForwardSearchActionBase
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

/**
 * Starts a forward search action in Okular.
 *
 * Note: this is only available on Linux.
 *
 * @author Abby Berkers
 */
open class ForwardSearchAction : ForwardSearchActionBase(PdfViewer.OKULAR)

package nl.hannahsten.texifyidea.action.preview

import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.wm.RegisterToolWindowTask
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.ui.EquationPreviewToolWindow
import nl.hannahsten.texifyidea.ui.PreviewFormUpdater
import nl.hannahsten.texifyidea.util.files.referencedFileSet
import java.util.EnumSet
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)

        val task = RegisterToolWindowTask(toolWindowId, contentFactory = PreviewToolWindowFactory(), icon = icon)
        // Avoid adding it twice
        val toolWindow = toolWindowManager.getToolWindow(toolWindowId) ?: toolWindowManager.registerToolWindow(task)

        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)
                content.displayName = displayName
                replaced = true
                break
            }
        }

        val contentFactory = ContentFactory.SERVICE.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)
        }
        // 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 com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory

class PreviewToolWindowFactory : ToolWindowFactory {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        // Default tool window content
    }
}

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.ui.PreviewFormUpdater
import nl.hannahsten.texifyidea.util.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
            preamble += 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.ui.PreviewFormUpdater
import nl.hannahsten.texifyidea.util.PackageUtils
import nl.hannahsten.texifyidea.util.environmentName
import nl.hannahsten.texifyidea.util.hasParent
import nl.hannahsten.texifyidea.util.parentOfType

/**
 * 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 of the tikz libs included in related packages (via \usetikzlibrary{}) to the produced document.
            val tikzLibs = PackageUtils.getIncludedTikzLibraries(psiFile)
            preamble += "\\usetikzlibrary{${tikzLibs.joinToString()}}\n"

            // Add all of the pgfplots libs included in related packages (via \usepgfplotslibrary{}) to the produced document.
            val pgfLibs = PackageUtils.getIncludedPgfLibraries(psiFile)
            preamble += "\\usepgfplotslibrary{${pgfLibs.joinToString()}}\n"

            preamble += 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 or not the outermost environment is tikz.
        return if (currElement.isTikz()) currElement else null
    }

    private fun LatexEnvironment.isTikz() = beginCommand.environmentName()
            ?.toLowerCase() == "tikzpicture"
}

package nl.hannahsten.texifyidea.action.skim

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer
import nl.hannahsten.texifyidea.settings.TexifySettings
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 = TexifySettings.getInstance().pdfViewer == PdfViewer.SKIM
    }
}

package nl.hannahsten.texifyidea.action.skim

import nl.hannahsten.texifyidea.action.ForwardSearchActionBase
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

/**
 * Starts a forward search action in Skim.
 *
 * Note: this is only available on MacOS.
 *
 * @author Stephan Sundermann
 */
open class ForwardSearchAction : ForwardSearchActionBase(PdfViewer.SKIM)

package nl.hannahsten.texifyidea.action.sumatra

import com.intellij.icons.AllIcons
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.isSumatraAvailable
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

                // 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
                    Runtime.getRuntime().exec("cmd.exe /c start SumatraPDF -inverse-search \"\\\"$path\\$name.exe\\\" --line %l \\\"%f\\\"\"")
                }
                else {
                    Runtime.getRuntime().exec("cmd.exe /c start SumatraPDF -inverse-search \"\\\"$path\\$name.exe\\\" \\\"\\\" --line %l \\\"%f\\\"\"")
                }

                dialogWrapper.close(0)
            }

            show()
        }
    }

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = isSumatraAvailable
    }
}

package nl.hannahsten.texifyidea.action.sumatra

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.action.EditorAction
import nl.hannahsten.texifyidea.run.sumatra.SumatraConversation
import nl.hannahsten.texifyidea.run.sumatra.isSumatraAvailable

/**
 * Starts a forward search action in SumatraPDF.
 *
 * This action sends a command to SumatraPDF which attempts to match the line where the cursor is at in the source to
 * the line in the PDF. Note: this is only available on Windows.
 *
 * @author Sten Wessel
 * @since b0.4
 */
open class ForwardSearchAction : EditorAction(
        "_Forward Search",
        TexifyIcons.RIGHT
) {

    override fun actionPerformed(file: VirtualFile, project: Project, textEditor: TextEditor) {
        if (!isSumatraAvailable) {
            return
        }

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

        try {
            SumatraConversation.forwardSearch(sourceFilePath = file.path, line = line)
        }
        catch (ignored: TeXception) {
        }
    }

    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = isSumatraAvailable
    }
}

package nl.hannahsten.texifyidea.action.tablewizard

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.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import nl.hannahsten.texifyidea.action.insert.InsertTable
import nl.hannahsten.texifyidea.lang.Package
import nl.hannahsten.texifyidea.ui.tablecreationdialog.ColumnType
import nl.hannahsten.texifyidea.ui.tablecreationdialog.TableCreationDialogWrapper
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.psiFile
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() {
    override fun actionPerformed(e: AnActionEvent) {
        val file = e.getData(PlatformDataKeys.VIRTUAL_FILE) ?: return
        val project = e.getData(PlatformDataKeys.PROJECT)
        val editors = FileEditorManager.getInstance(project!!).selectedEditors
        val editor = editors.first { it is TextEditor } as TextEditor
        val document = editor.editor.document

        // Get the indentation from the current line.
        val indent = document.lineIndentation(document.getLineNumber(editor.editor.caretOffset()))

        // Create the dialog.
        val dialogWrapper = TableCreationDialogWrapper()
        // If the user pressed OK, do stuff.
        if (dialogWrapper.showAndGet()) {

            // Get the table information from the dialog, and convert it to latex.
            val tableTextToInsert = convertTableToLatex(dialogWrapper.tableInformation, 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",
                    Runnable { file.psiFile(project)!!.insertUsepackage(Package.BOOKTABS) },
                    file.psiFile(project)
            )
        }
    }

    /**
     * Convert the table information to a latex table that can be inserted into the file.
     *
     * @param tableInformation
     * @param lineIndent is the indentation of the current line, to be used on each new line.
     * @param tabIndent is the continuation indent.
     */
    private fun convertTableToLatex(tableInformation: TableInformation, lineIndent: String, tabIndent: String = "    "): String {
        /**
         * Local 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.
         */
        fun processTableContent(indent: String): String {
            return with(tableInformation) {
                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
                    }
                }
                headers + rows
            }
        }

        // 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).
        return with(tableInformation) {
            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.

            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.tablewizard

import nl.hannahsten.texifyidea.ui.tablecreationdialog.ColumnType
import nl.hannahsten.texifyidea.ui.tablecreationdialog.TableCreationTableModel

/**
 * Stores all information about a table.
 *
 * @param tableModel contains all information about the contents of the table. That is, the column names and the table
 * entries.
 * @param columnTypes contains the type of each column.
 * @param caption contains the caption to go along with the table.
 * @param label contains the label that is to be used to reference to the table.
 *
 * @author Abby Berkers
 */
data class TableInformation(
        val tableModel: TableCreationTableModel,
        val columnTypes: List<ColumnType>,
        val caption: String,
        val label: String
)

package nl.hannahsten.texifyidea.action.zathura

import nl.hannahsten.texifyidea.action.ForwardSearchActionBase
import nl.hannahsten.texifyidea.run.linuxpdfviewer.PdfViewer

class ForwardSearchAction : ForwardSearchActionBase(PdfViewer.ZATHURA)

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.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 != "\\include" && command.name != "\\bibliography") 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.Argument
import nl.hannahsten.texifyidea.lang.LatexMathCommand
import nl.hannahsten.texifyidea.lang.LatexRegularCommand
import nl.hannahsten.texifyidea.lang.RequiredArgument
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.endOffset
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.parentOfType

/**
 * @author Hannah Schellekens
 */
class LatexCommandArgumentInsertHandler(val arguments: List<Argument>? = null) : InsertHandler<LookupElement> {
    override fun handleInsert(insertionContext: InsertionContext, lookupElement: LookupElement) {
        removeWhiteSpaces(insertionContext)

        when (val `object` = lookupElement.getObject()) {
            is LatexCommands -> {
                insertCommands(insertionContext, lookupElement)
            }
            is LatexMathCommand -> {
                insertMathCommand(`object`, insertionContext, lookupElement)
            }
            is LatexRegularCommand -> {
                insertNoMathCommand(`object`, insertionContext, lookupElement)
            }
        }
    }

    private fun insertCommands(context: InsertionContext, lookupElement: LookupElement) {
        insert(context, lookupElement)
    }

    private fun insertMathCommand(mathCommand: LatexMathCommand, context: InsertionContext, lookupElement: LookupElement) {
        if (mathCommand.autoInsertRequired()) {
            insert(context, lookupElement)
        }
    }

    private fun insertNoMathCommand(noMathCommand: LatexRegularCommand, context: InsertionContext, lookupElement: LookupElement) {
        if (noMathCommand.autoInsertRequired()) {
            insert(context, 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)
        }
    }

    /**
     * Remove whitespaces that are inserted by the lookup text...
     */
    private fun removeWhiteSpaces(context: InsertionContext) {
        val editor = context.editor
        val document = editor.document
        val offset = editor.caretModel.offset
        val textUntilOffset = document.text.run { dropLast(length - offset) }
        val indexOfLastChar = textUntilOffset.indexOfLast { it != ' ' }
        document.deleteString(indexOfLastChar + 1, offset)
    }
}

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.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.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.Argument
import nl.hannahsten.texifyidea.lang.Environment
import nl.hannahsten.texifyidea.lang.LatexCommand
import nl.hannahsten.texifyidea.lang.RequiredArgument
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.definitionsAndRedefinitionsInFileSet

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexNoMathInsertHandler(val arguments: List<Argument>? = null) : InsertHandler<LookupElement> {

    override fun handleInsert(context: InsertionContext, item: LookupElement) {
        val command = item.`object` as LatexCommand

        when (command.command) {
            "begin" -> {
                insertBegin(context)
            }
            in Magic.Typography.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)
                ?.firstOrNull()?.arguments
                ?.count { it is RequiredArgument } ?: 0

        val templateText = List(numberRequiredArguments) {
            "{\$__Variable${it}\$}"
        }.joinToString("") + "\n\$END\$\n\\${Magic.Typography.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)
    }

    /**
     * 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.name) && 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

/**
 * @author Sten Wessel
 */
class LatexReferenceInsertHandler : InsertHandler<LookupElement> {
    override fun handleInsert(context: InsertionContext, item: LookupElement) {
        val model = context.editor.caretModel
        model.moveToOffset(model.offset + 1)
    }
}

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.endOffset
import nl.hannahsten.texifyidea.util.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.LatexCommand
import nl.hannahsten.texifyidea.util.Magic

/**
 * 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

        if (name.startsWith("left")) {
            insertRightBraceCommand(name, editor)
        }
        else if (name == "langle") {
            insertRightCommand(editor, "\\rangle", spacing = " ")
        }
        else if (name == "lq") {
            insertRightCommand(editor, "\\rq", spacing = "", suffix = "{}")
        }
    }

    private fun insertRightBraceCommand(commandName: String, editor: Editor) {
        val char = commandName.substring(4)
        val opposite = Magic.Typography.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
        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.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.findVirtualFileByAbsoluteOrRelativePath
import nl.hannahsten.texifyidea.util.files.getGraphicsPaths

/**
 * Autocompletion roots based on graphicspaths.
 */
class LatexGraphicsPathProvider : LatexPathProviderBase() {
    override fun selectScanRoots(file: PsiFile): ArrayList<VirtualFile> {
        val paths = getProjectRoots()

        val rootDirectory = file.findRootFile().containingDirectory?.virtualFile
        getGraphicsPaths(file.project).forEach {
            paths.add(rootDirectory?.findVirtualFileByAbsoluteOrRelativePath(it) ?: return@forEach)
        }

        return paths
    }

    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.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.RequiredFileArgument
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.text)
        resultSet = result.withPrefixMatcher(autocompleteText)

        val parentCommand = context.get("type")
        if (parentCommand is RequiredFileArgument) {
            validExtensions = parentCommand.supportedExtensions
            absolutePathSupport = parentCommand.isAbsolutePathSupported
        }

        selectScanRoots(parameters.originalFile).forEach {
            addByDirectory(it, autocompleteText)
        }
    }

    /**
     * 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) {
        LocalFileSystem.getInstance().findFileByPath(projectDir.path + "/" + 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.first() }
                    .toSet()
                    .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
     */
    private fun processAutocompleteText(autocompleteText: String): String {
        var result = if (autocompleteText.endsWith("}")) {
            autocompleteText.substring(0, autocompleteText.length - 1)
        }
        else autocompleteText

        // 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(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

/**
 * 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) {
                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 (project != null && TexifySettings.getInstance().autoCompile) {
            AutoCompileState.documentChanged(project)
        }
    }

    override fun charDeleted(c: Char, file: PsiFile, editor: Editor): Boolean {
        return true
    }
}

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("AutoCompileState", "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("AutoCompileState", "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.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.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 {
        ContinuousPreviewHandler().charTyped(c, file.project, editor, file)
        return true
    }
}

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

object 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.firstParentOfType
import nl.hannahsten.texifyidea.util.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.Package
import nl.hannahsten.texifyidea.util.insertUsepackage

abstract class LatexPostfixTemplateFromPackageProvider(private val pack: Package) : 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: Package?): PostfixTemplateProvider {
            return when (pack) {
                Package.AMSMATH -> LatexPostfixTemplateFromAmsMathProvider
                Package.AMSFONTS -> LatexPostfixTemplateFromAmsFontsProvider
                Package.BM -> LatexPostfixTemplateFromBmProvider
                else -> LatexPostFixTemplateProvider
            }
        }
    }
}

object LatexPostfixTemplateFromAmsMathProvider : LatexPostfixTemplateFromPackageProvider(Package.AMSMATH) {
    override fun getTemplates(): MutableSet<PostfixTemplate> = mutableSetOf(
            LatexWrapWithTextPostfixTemplate
    )
}

object LatexPostfixTemplateFromAmsFontsProvider : LatexPostfixTemplateFromPackageProvider(Package.AMSFONTS) {
    override fun getTemplates(): MutableSet<PostfixTemplate> = mutableSetOf(
            LatexWrapWithMathbbPostfixTemplate
    )
}

object LatexPostfixTemplateFromBmProvider : LatexPostfixTemplateFromPackageProvider(Package.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.Package

/**
 * 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 = Package.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 = Package.AMSFONTS)
internal object LatexWrapWithBmPostfixTemplate : LatexWrapWithCommandPostfixTemplate("bm", mathOnly = true, pack = Package.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: Package? = 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: Package? = 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

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()
    )
}

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.PsiDocumentManager
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.endOffset

/**
 * Surrounds selected text with the [surroundPair].
 */
open class LatexSurrounder(private val before: String, private val after: String) : 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.
     */
    override fun surroundElements(project: Project, editor: Editor, elements: Array<out PsiElement>): TextRange? {
        val beforeNode = LatexPsiHelper(project).createFromText(before).firstChild.node
        val afterNode = LatexPsiHelper(project).createFromText(after).firstChild.node
        elements.first().parent.node.addChild(beforeNode, elements.first().node)
        elements.last().parent.node.addChild(afterNode, elements.last().nextSibling?.node)
        // Reparse the document because this introduces parse errors :(
        PsiDocumentManager.getInstance(project).reparseFiles(mutableSetOf(elements.first().containingFile.virtualFile), true)
        // Not sure why the -1 is needed...
        val endOffset = elements.last().endOffset() + before.length + after.length - 1
        return TextRange(endOffset, endOffset)
    }

    override fun getTemplateDescription(): String {
        return "Surround with $before$after"
    }
}

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.file.listeners

import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.files.ReferencedFileSetService

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 Magic.File.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.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.util.Magic
import nl.hannahsten.texifyidea.util.firstParentOfType
import nl.hannahsten.texifyidea.util.getIndent
import kotlin.math.min

/**
 * Align spaces to the right of &
 */
fun rightTableSpaceAlign(latexCommonSettings: CommonCodeStyleSettings, parent: ASTBlock, left: ASTBlock): Spacing? {

    if (parent.node?.psi?.firstParentOfType(LatexEnvironmentContent::class)
                    ?.firstParentOfType(LatexEnvironment::class)?.environmentName !in Magic.Environment.tableEnvironments) return null

    if (left.node?.text?.endsWith("&") == false) 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)
    if (contentElement?.firstParentOfType(LatexEnvironment::class)?.environmentName !in Magic.Environment.tableEnvironments) 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)

    val spaces = getNumberOfSpaces(contentWithoutRules, tableLineSeparator, right, absoluteAmpersandIndicesPerLine, indent)
            ?: return null

    return createSpacing(
            minSpaces = spaces,
            maxSpaces = spaces,
            minLineFeeds = 0,
            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) ->
            if (it == '&') 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)
}

/**
 * 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 {
                value == '&' -> {
                    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) {
                        val isAfterSpaceOrSeparator = line[i - 1] in setOf(' ', '&', '\n')
                        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.map { it.size }.max() ?: 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 {
        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[i].ignore()) 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.map { it[level] }.max() ?: 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[i].ignore()) 1
            // Add 1 for the space that always has to be there.
            else cellWidths[level] - cellWidth + 1
        }
    }
}

/**
 * Find level of 'right' block (which is a & or \\)
 */
private fun getSpacesForRightBlock(right: ASTBlock, absoluteAmpersandIndicesPerLine: List<List<Int>>, spacesPerCell: 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) {
                return spacesPerCell[min(i, spacesPerCell.size - 1)][level]
            }
        }
    }
    return null
}

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.BibtexLanguage
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.psi.impl.BibtexEntryImpl

open class BibtexEntryStubElementType(private val 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.identifier, entry.authors, entry.year, entry.title)
    }

    override fun getExternalId() = debugName

    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: List<String>
}

package nl.hannahsten.texifyidea.index.stub

import com.intellij.psi.stubs.*
import nl.hannahsten.texifyidea.LatexLanguage
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.psi.impl.LatexCommandsImpl
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.getIncludeCommands
import java.io.IOException
import java.util.LinkedList
import java.util.regex.Pattern
import java.util.stream.Collectors

/**
 * @author Hannah Schellekens
 */
class LatexCommandsStubElementType(debugName: String) :
    IStubElementType<LatexCommandsStub, LatexCommands>(debugName, LatexLanguage.INSTANCE) {
    override fun createPsi(latexCommandsStub: LatexCommandsStub): LatexCommands {
        return object : LatexCommandsImpl(latexCommandsStub, this) {
            init {
                setName(latexCommandsStub.name!!)
            }
        }
    }

    override fun createStub(latexCommands: LatexCommands, parent: StubElement<*>?): LatexCommandsStub {
        val commandToken = latexCommands.commandToken.text
        latexCommands.setName(commandToken)
        val requiredParameters = latexCommands.requiredParameters
        val optionalParameters: List<String> =
            LinkedList(latexCommands.optionalParameters.keys)
        return LatexCommandsStubImpl(
            parent!!, this,
            commandToken,
            requiredParameters,
            optionalParameters
        )
    }

    override fun getExternalId(): String {
        return "texify.latex.commands"
    }

    @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 = deserialiseList(stubInputStream.readName().toString())
        return LatexCommandsStubImpl(
            parent, this,
            name,
            required,
            optional
        )
    }

    override fun indexStub(latexCommandsStub: LatexCommandsStub, indexSink: IndexSink) {
        indexSink.occurrence(
            LatexCommandsIndex.key(),
            latexCommandsStub.commandToken
        )
        val token = latexCommandsStub.commandToken
        if (getIncludeCommands().contains(token)) {
            indexSink.occurrence(LatexIncludesIndex.key(), token)
        }
        if (Magic.Command.definitions.contains(token) || Magic.Command.redefinitions.contains(token)) {
            indexSink.occurrence(LatexDefinitionIndex.key(), token)
        }
    }

    private fun deserialiseList(string: String): List<String> {
        return SEPERATOR.splitAsStream(string)
            .collect(Collectors.toList())
    }

    private fun serialiseRequired(stub: LatexCommandsStub): String {
        return java.lang.String.join(SEPERATOR.pattern(), stub.requiredParams)
    }

    private fun serialiseOptional(stub: LatexCommandsStub): String {
        return java.lang.String.join(SEPERATOR.pattern(), stub.optionalParams)
    }

    companion object {
        private val SEPERATOR =
            Pattern.compile("\u1923\u9123\u2d20 hello\u0012")
    }
}

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: List<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.LatexLanguage
import nl.hannahsten.texifyidea.index.LatexEnvironmentsIndex
import nl.hannahsten.texifyidea.index.LatexParameterLabeledEnvironmentsIndex
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.impl.LatexEnvironmentImpl
import nl.hannahsten.texifyidea.util.Magic
import java.io.IOException

open class LatexEnvironmentStubElementType(private val debugName: String) : IStubElementType<LatexEnvironmentStub, LatexEnvironment>(debugName, LatexLanguage.INSTANCE) {
    override fun createPsi(stub: LatexEnvironmentStub): LatexEnvironment {
        return LatexEnvironmentImpl(stub, this)
    }

    override fun createStub(psi: LatexEnvironment, parentStub: StubElement<*>): LatexEnvironmentStub {
        return LatexEnvironmentStubImpl(parentStub, this, psi.environmentName, psi.label ?: "")
    }

    override fun getExternalId() = debugName

    @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) {
        sink.occurrence(LatexEnvironmentsIndex.key(), stub.environmentName)

        // only record environments with a label in the optional parameters
        if (stub.label.isNotEmpty() && Magic.Environment.labelAsParameter.contains(stub.environmentName)) {
            sink.occurrence(LatexParameterLabeledEnvironmentsIndex.key(), 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.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 nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document

/**
 * @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 == "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,
                                isOntheFly,
                                RemoveOtherCommandsFix(fileName, commands)
                        ))
                    }
                }

        return descriptors
    }

    /**
     * @author Sten Wessel
     */
    class RemoveOtherCommandsFix(private val bibName: String, private val commandsToFix: List<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.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.insight.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.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 == "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,
                                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.insight.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.findLatexCommandsLabels
import nl.hannahsten.texifyidea.util.identifier
import nl.hannahsten.texifyidea.util.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.identifier() }.toList()

        val added = HashSet<BibtexEntry>()
        // Check the bibtexIds in the current file
        for (bibtexEntry in BibtexEntryIndex.getIndexedEntries(file)) {
            val idName = bibtexEntry.identifier()

            // 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,
                        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,
                        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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.endOffset
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.openedEditor
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.openedEditor() ?: 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.latex

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.LatexCommand
import nl.hannahsten.texifyidea.lang.RequiredArgument
import nl.hannahsten.texifyidea.lang.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.requiredParameters.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.requiredParameters[i])
                if (offset == -1) continue
                val range = TextRange(0, command.requiredParameters[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,
                            isOntheFly
                    ))
                }
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
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

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

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 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)
            Magic.Pattern.abbreviation.toRegex().find(subString)?.groups?.isNotEmpty() == true || Magic.General.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

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.insight.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.LatexContent
import nl.hannahsten.texifyidea.psi.LatexParameterText
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
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()
        for (cmd in commands) {
            if (cmd.name !in Magic.Command.bibliographyReference) {
                continue
            }

            val bundle = cmd.findCiteBundle()
            if (bundle.size <= 1) {
                continue
            }

            descriptors.add(manager.createProblemDescriptor(
                    cmd,
                    "Citations can be collapsed",
                    InspectionFix(bundle),
                    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? LatexContent }

    private fun LatexCommands.previousCite() = searchCite { it.previousSiblingIgnoreWhitespace() as? LatexContent }

    private inline fun LatexCommands.searchCite(nextThing: (LatexContent) -> LatexContent?): LatexCommands? {
        val content = grandparent(2) as? LatexContent ?: return null
        val nextContent = nextThing(content) ?: return null

        var cite = nextContent.firstChildOfType(LatexCommands::class)
        if (cite == null) {
            if (!nextContent.isCorrect()) {
                return null
            }

            val secondNextContent = nextThing(nextContent) ?: return null
            cite = secondNextContent.firstChildOfType(LatexCommands::class) ?: return null
        }

        val name = cite.name ?: return null
        val nextCommandIsACitation = name in Magic.Command.bibliographyReference
        val previousCommandIsOfTheSameType = this.name == name
        val equalStars = hasStar() == cite.hasStar()
        return if (nextCommandIsACitation && previousCommandIsOfTheSameType && equalStars) cite else null
    }

    private fun LatexContent.isCorrect(): Boolean {
        val normalText = firstChildOfType(LatexParameterText::class) ?: return false
        return normalText.text.length == 1
    }

    /**
     * @author Hannah Schellekens
     */
    private inner class InspectionFix(val citeBundle: List<LatexCommands>) : LocalQuickFix {

        override fun getFamilyName(): String {
            return "Collapse citations"
        }

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val file = descriptor.psiElement.containingFile ?: return
            val document = file.document() ?: return
            val sortedBundle = citeBundle.sortedBy { it.textOffset }
            val offsetRange = sortedBundle.first().textOffset until sortedBundle.last().endOffset()

            val bundle = sortedBundle
                    .flatMap { it.requiredParameters }
                    .joinToString(",")

            val first = citeBundle.first()
            val star = if (first.hasStar()) "*" else ""
            document.replaceString(offsetRange.toTextRange(), "${first.name}$star{$bundle}")
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.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.forcedFirstRequiredParameterAsCommand
import nl.hannahsten.texifyidea.util.isKnown
import nl.hannahsten.texifyidea.util.replaceString
import java.util.*

/**
 * @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 is already defined",
                            true,
                            ProblemHighlightType.GENERIC_ERROR,
                            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 is already defined",
                            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

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.hasParent
import nl.hannahsten.texifyidea.util.inMathContext
import nl.hannahsten.texifyidea.util.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

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.insight.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.LatexPsiUtil
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
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)

            document.replaceString(startOFfset, endOffset, "$commandName{${cmd.text}}{${value.text}}")
        }

        open fun getArguments(command: LatexCommands): Pair<PsiElement, PsiElement>? {
            val parent = command.parent.parent
            val firstSib = LatexPsiUtil.getNextSiblingIgnoreWhitespace(parent) ?: return null
            val secondSib = LatexPsiUtil.getNextSiblingIgnoreWhitespace(firstSib) ?: return null
            return Pair(firstSib, secondSib)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.definedCommandName
import nl.hannahsten.texifyidea.util.files.definitions
import nl.hannahsten.texifyidea.util.files.definitionsInFileSet

/**
 * @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 Magic.Command.regularCommandDefinitions }
        for (command in definitions) {
            val name = command.definedCommandName() ?: continue
            defined.add(name)
        }

        // Go monkeys.
        file.definitions()
                .filter { it.name in Magic.Command.regularStrictCommandDefinitions }
                .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,
                                isOntheFly
                        ))
                    }
                }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.CommandManager
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexParameter
import nl.hannahsten.texifyidea.util.findBibitemCommands
import nl.hannahsten.texifyidea.util.findLabelingCommandsInFileSetAsSequence
import nl.hannahsten.texifyidea.util.parentOfType
import nl.hannahsten.texifyidea.util.requiredParameter
import java.lang.Integer.max
import java.util.EnumSet

/**
 * @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.findLabelingCommandsInFileSetAsSequence(), isOntheFly, manager, file) {
            val name = this.name ?: return@getProblemDescriptors null
            val position = CommandManager.labelAliasesInfo.getOrDefault(name, null)?.positions?.firstOrNull() ?: return@getProblemDescriptors null
            this.requiredParameter(position) ?: return@getProblemDescriptors null
        }

        val duplicateBibitems = getProblemDescriptors(file.findBibitemCommands(), isOntheFly, manager, file) {
            this.requiredParameter(0) ?: return@getProblemDescriptors null
        }

        return duplicateLabels + duplicateBibitems
    }

    /**
     * @param getLabel Get the label string given the command in which it is defined.
     */
    private fun getProblemDescriptors(commands: Sequence<LatexCommands>, isOntheFly: Boolean, manager: InspectionManager, file: PsiFile, getLabel: LatexCommands.() -> String?): List<ProblemDescriptor> {

        // Map labels to commands defining the label
        val allLabels = hashMapOf<String, MutableSet<LatexCommands>>()

        val result = mutableListOf<ProblemDescriptor>()

        // Treat LaTeX and BibTeX separately because only the first parameter of \bibitem commands counts
        // First find all duplicate labels
        commands.forEach { command ->
            // When the label is defined in \newcommand ignore it, because there could be more than one with #1 as parameter
            val parent = command.parentOfType(LatexCommands::class)
            if (parent != null && parent.name == "\\newcommand") {
                return@forEach
            }

            allLabels.getOrPut(command.getLabel() ?: return@forEach) { mutableSetOf() }.add(command)
        }

        val duplicates = allLabels.filter { it.value.size > 1 }

        for ((label, labelCommands) in duplicates) {
            // We can only mark labels in the current file
            val currentFileDuplicates = labelCommands.filter { command -> command.containingFile == file }

            for (command in currentFileDuplicates) {
                result.add(createProblemDescriptor(command, label, isOntheFly, manager))
            }
        }

        return result
    }

    /**
     * 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)).map { it.length }
                .sum()
    }

    /**
     * make the mapping from command etc. to ProblemDescriptor
     */
    private fun createProblemDescriptor(cmd: LatexCommands, label: String, isOntheFly: Boolean, manager: InspectionManager):
            ProblemDescriptor {
        val offset = cmd.commandToken.textLength + skippedParametersLength(cmd.parameterList, label) + 1
        return manager.createProblemDescriptor(
                cmd,
                TextRange.from(offset, label.length),
                "Duplicate label '$label'",
                ProblemHighlightType.GENERIC_ERROR,
                isOntheFly
        )
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.startOffset
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSMATH
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.*

/**
 * @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"

    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 Magic.Pattern.ellipsis.findAll(text.text)) {
                // Ignore the inspection when the ellipsis is inside a comment.
                if (file.findElementAt(match.range.first + text.startOffset)?.isComment() == true) {
                    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.name !in file.includedPackages()) {
                file.insertUsepackage(AMSMATH)
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.length
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, _ -> Magic.Pattern.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

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.PsiComment
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import nl.hannahsten.texifyidea.insight.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.util.*
import nl.hannahsten.texifyidea.util.files.document
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

                descriptors.add(manager.createProblemDescriptor(
                        openElement,
                        TextRange.from(0, 1),
                        "Parentheses pair could be replaced by \\left(..\\right)",
                        ProblemHighlightType.WEAK_WARNING,
                        isOntheFly,
                        InsertLeftRightFix(openOffset, closeOffset, document[openOffset])
                ))

                descriptors.add(manager.createProblemDescriptor(
                        closeElement,
                        TextRange.from(0, 1),
                        "Parentheses pair could be replaced by \\left(..\\right)",
                        ProblemHighlightType.WEAK_WARNING,
                        isOntheFly,
                        InsertLeftRightFix(openOffset, closeOffset, document[openOffset])
                ))
            }
        }

        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 && nested <= 0) {
                closeOffset = current
                break
            }
        }

        if (!foundAffectedCommand || closeOffset == null) {
            return null
        }

        return closeOffset
    }

    private fun affectedCommands() = Magic.Command.high + lines

    /**
     * @author Hannah Schellekens
     */
    private open class InsertLeftRightFix(val openOffset: Int, val closeOffset: Int, val open: String) : LocalQuickFix {

        override fun getFamilyName() = "Convert (..) to \\left(..\\right)"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val file = descriptor.psiElement.containingFile
            val document = file.document() ?: return

            document[closeOffset] = "\\right${brackets[open]}"
            document[openOffset] = "\\left$open"
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.lang.Package
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.findLabelsInFileSetAsCollection
import nl.hannahsten.texifyidea.util.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.findLabelsInFileSetAsCollection().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(Package.AMSMATH)
    }
}

package nl.hannahsten.texifyidea.inspections.latex

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.*
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.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(Magic.Environment.tableEnvironments)) return true
        if (this.inDirectEnvironment(Magic.Environment.alignableEnvironments)) return true

        // Do not trigger in URLs.
        if (this.firstParentOfType(LatexCommands::class)?.name in Magic.Command.urls) return true

        return false
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.isCommandDefinition
import nl.hannahsten.texifyidea.util.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.isCommandDefinition() }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

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.Magic
import nl.hannahsten.texifyidea.util.firstParentOfType
import nl.hannahsten.texifyidea.util.inMathContext
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.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)
    }

    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 = Magic.Command.sectionMarkers + Magic.Command.textStyles + setOf("""\caption""")
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.ProblemDescriptor
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.lang.Package.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

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.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.*
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import java.util.EnumSet

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))
        }

        return descriptors
    }

    private fun removeReferencedLabels(file: PsiFile, figureLabels: MutableMap<String?, LatexCommands>) {
        val referenceCommands = Magic.Command.getLabelReferenceCommands(file.project)
        for (command in file.commandsInFileSet()) {
            // Don't resolve references in command definitions
            if (command.parent.firstParentOfType(LatexCommands::class)?.name in Magic.Command.commandDefinitions ||
                referenceCommands.contains(command.name)) {
                command.referencedLabelNames.forEach { figureLabels.remove(it) }
            }
        }
    }

    private fun createDescriptor(manager: InspectionManager, label: LatexCommands, isOntheFly: Boolean): ProblemDescriptor =
            manager.createProblemDescriptor(
                    label,
                    "Figure is not referenced",
                    isOntheFly,
                    emptyArray(),
                    ProblemHighlightType.WEAK_WARNING
            )

    private fun getFigureLabels(file: PsiFile): MutableMap<String?, LatexCommands> =
            file.findLabelingCommandsInFileAsSequence()
                    .filter(this::isFigureLabel)
                    .associateBy(LatexCommands::labelName)
                    .toMutableMap()

    private fun isFigureLabel(label: LatexCommands): Boolean =
            label.inDirectEnvironment(Magic.Environment.figures)
}

private val LatexCommands.labelName: String?
    get() = requiredParameter(0)

private val LatexCommands.referencedLabelNames: List<String>
    get() = requiredParameter(0)?.split(",") ?: emptyList()

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.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.Magic.Command.illegalExtensions
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.createFile
import nl.hannahsten.texifyidea.util.files.findRootFile
import nl.hannahsten.texifyidea.util.files.getFileExtension
import java.io.File
import java.util.EnumSet

/**
 * @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.firstParentOfType(LatexCommands::class)?.name in Magic.Command.commandDefinitions) {
                continue
            }

            val referencesList = command.references.filterIsInstance<InputFileReference>()
            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.toLowerCase() }
            if (reference.key in ctanPackages) return
        }

        val fixes = mutableListOf<LocalQuickFix>()

        // Create quick fixes for all extensions
        extensions.forEach {
            fixes.add(CreateNewFileWithDialogQuickFix(fileName, it, reference))
        }

        // Find expected extension
        val extension = if (fileName.getFileExtension().isNotEmpty()) {
            fileName.getFileExtension()
        }
        else {
            reference.defaultExtension
        }

        descriptors.add(manager.createProblemDescriptor(
                reference.element,
                reference.rangeInElement,
                "File '${fileName.appendExtension(extension)}' not found",
                ProblemHighlightType.GENERIC_ERROR,
                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 reference: InputFileReference) : 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 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.
            val newFilePath = CreateFileDialog(file.containingDirectory?.virtualFile?.canonicalPath, filePath.formatAsFilePath())
                    .newFileFullPath ?: return

            runWriteAction {
                val createdFile = createFile("$newFilePath.$extension", "")

                // Update LaTeX command parameter with chosen filename
                var fileNameRelativeToRoot = createdFile.absolutePath
                        .replace(File.separator, "/")
                        .replace("$root/", "")

                val command = (cmd as? LatexCommands)?.name
                if (command in illegalExtensions) {
                    illegalExtensions[command]?.forEach { fileNameRelativeToRoot = fileNameRelativeToRoot.replace(it, "") }
                }

                reference.handleElementRename(fileNameRelativeToRoot, false)
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSMATH
import nl.hannahsten.texifyidea.psi.LatexContent
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document

/**
 * @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(LatexContent::class).asSequence()
                .filter { it.isDisplayMath() }
                .map { Pair(it, it.nextSiblingIgnoreWhitespace()) }
                .filter { (_, next) -> next != null && next is LatexContent && next.isDisplayMath() }
                .flatMap { sequenceOf(it.first, it.second) }
                .distinct()
                .forEach {
                    descriptors.add(manager.createProblemDescriptor(
                            it ?: return@forEach,
                            "Equations can be gathered",
                            GatherEnvironments(),
                            ProblemHighlightType.WEAK_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? LatexContent ?: 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: LatexContent) = equation.text
                // Remove \[ and \]
                .trimRange(2, 2)
                // Remove whitespace
                .trim()

        /**
         * Finds all adjacent equations.
         */
        private fun findEquations(base: LatexContent): List<LatexContent> {
            val equations = ArrayList<LatexContent>()
            equations.add(base)

            // Lookbehind.
            var content = base.previousSiblingIgnoreWhitespace()
            while (content is LatexContent && content.isDisplayMath()) {
                equations.add(0, content)
                content = content.previousSiblingIgnoreWhitespace()
            }

            // Lookahead.
            content = base.nextSiblingIgnoreWhitespace()
            while (content is LatexContent && content.isDisplayMath()) {
                equations.add(content)
                content = content.nextSiblingIgnoreWhitespace()
            }

            return equations
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

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.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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.util.files.findIncludedFile
import nl.hannahsten.texifyidea.util.files.searchFileByImportPaths
import java.util.*

/**
 * 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()

        // See Project.allFileInclusions()

        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

            // Check if import package is used
            val fileMaybe = searchFileByImportPaths(command)
            if (fileMaybe != null) {
                inclusions.getOrPut(declaredIn) { mutableSetOf() }.add(fileMaybe)
            }
            else {
                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,
                                isOntheFly
                        ))
                    }
                }
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.files.openedEditor
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.WEAK_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.openedEditor()?.caretModel
            document.insertString(offset, replacement)
            caret?.moveToOffset(offset + newParentCommand.length + 1)
        }
    }

    private class ChangeToParentCommandFix : LocalQuickFix {

        override fun getFamilyName() = "Change to parent command"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val document = command.containingFile.document() ?: return
            val range = command.commandToken.textRange
            val newParentCommand = command.commandToken.text.replaceFirst("sub", "")
            document.replaceString(range, newParentCommand)
        }
    }

    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

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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.CommandManager
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.*
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.commandsAndFilesInFileSet
import java.util.EnumSet
import kotlin.reflect.jvm.internal.impl.utils.SmartList

/**
 * 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 (label.inDirectEnvironmentMatching {
                                Magic.Environment.labeled.containsKey(it.environmentName) &&
                                        !Magic.Environment.labelAsParameter.contains(it.environmentName)
                            }) {
                        label.parentOfType(LatexEnvironment::class)
                    }
                    else {
                        val grandparent = label.parent.parent
                        val sibling = LatexPsiUtil.getPreviousSiblingIgnoreWhitespace(grandparent) ?: 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? {
            return when (labeledCommand) {
                is LatexCommands -> {
                    Magic.Command.labeledPrefixes.getOrDefault(labeledCommand.name, null)
                }
                is LatexEnvironment -> {
                    Magic.Environment.labeled.getOrDefault(labeledCommand.environmentName, null)
                }
                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 = SmartList<ProblemDescriptor>()
        checkLabels(file, manager, isOntheFly, descriptors)
        return descriptors
    }

    private fun checkLabels(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean,
                            descriptors: MutableList<ProblemDescriptor>) {
        file.findLatexLabelPsiElementsInFileAsSequence().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.WEAK_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(":")) {
                Magic.Pattern.labelPrefix.matcher(labelName).replaceAll("$prefix:")
            }
            else {
                "$prefix:$labelName"
            }

            val createdLabel = appendCounter(createdLabelBase, baseFile.findLatexAndBibtexLabelStringsInFileSet())

            // Replace in command label definition
            if (command is LatexCommands) {
                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.replaceOptionalParameter(command.beginCommand.parameterList, "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 { Magic.Command.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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.document
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 = Magic.Pattern.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)

                // Do not trigger the inspection when in a comment.
                if (file.findElementAt(startOffset + text.startOffset)?.isComment() == true) {
                    continue
                }

                descriptors.add(manager.createProblemDescriptor(
                        text,
                        TextRange(startOffset, min(text.textLength, endOffset)),
                        "Sentence does not start on a new line",
                        ProblemHighlightType.WEAK_WARNING,
                        isOntheFly,
                        InspectionFix()
                ))
            }
        }

        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 = Magic.Pattern.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

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * @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)} shouldn't be used in tex sources" },
        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?.toLowerCase() == "tex" && super.checkContext(matcher, element)
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexMathContent
import nl.hannahsten.texifyidea.psi.LatexTypes
import nl.hannahsten.texifyidea.util.Magic
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
        get() = InsightGroup.LATEX

    @Nls
    override fun getDisplayName(): String {
        return "Non-escaped common math operators"
    }

    override val inspectionId: String
        get() = "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)) {
                        if (Magic.Command.slashlessMathOperators.contains(element.text)) {
                            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 {
            return "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, "\\")
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.forcedFirstRequiredParameterAsCommand
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 (Magic.Command.redefinitions.contains(command.name)) {
                val newCommand = command.forcedFirstRequiredParameterAsCommand() ?: continue
                if (Magic.Command.fragile.contains(newCommand.name)) {
                    descriptors.add(
                        manager.createProblemDescriptor(
                            command,
                            "This might break TeXiFy functionality",
                            null as LocalQuickFix?,
                            ProblemHighlightType.GENERIC_ERROR,
                            isOntheFly
                        )
                    )
                }
            }
        }
        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.insight.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.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,
                        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

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.insight.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,
                            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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.DefaultEnvironment
import nl.hannahsten.texifyidea.lang.LatexCommand
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSFONTS
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSMATH
import nl.hannahsten.texifyidea.lang.Package.Companion.AMSSYMB
import nl.hannahsten.texifyidea.lang.Package.Companion.DEFAULT
import nl.hannahsten.texifyidea.lang.Package.Companion.MATHTOOLS
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.*
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.definitionsAndRedefinitionsInFileSet
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 = PackageUtils.getIncludedPackages(file)
        analyseCommands(file, includedPackages, descriptors, manager, isOntheFly)
        analyseEnvironments(file, includedPackages, descriptors, manager, isOntheFly)

        return descriptors
    }

    private fun analyseEnvironments(file: PsiFile, includedPackages: Collection<String>,
                                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()

        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.name)) {
                continue
            }

            // amsfonts is included in amssymb
            if (pack == AMSFONTS && includedPackages.contains(AMSSYMB.name)) {
                continue
            }

            // amsmath is included in mathtools
            if (pack == AMSMATH && includedPackages.contains(MATHTOOLS.name)) {
                continue
            }

            descriptors.add(manager.createProblemDescriptor(
                    env,
                    TextRange(7, 7 + name.length),
                    "Environment requires ${pack.name} package",
                    ProblemHighlightType.ERROR,
                    isOntheFly,
                    ImportEnvironmentFix(pack.name)
            ))
        }
    }

    private fun analyseCommands(file: PsiFile, includedPackages: Collection<String>,
                                descriptors: MutableList<ProblemDescriptor>, manager: InspectionManager,
                                isOntheFly: Boolean) {
        val commands = file.commandsInFile()
        commandLoop@ for (command in commands) {
            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 Magic.Package.packagesLoadingOtherPackages) {
                if (packageInclusion.value.intersect(dependencies).isNotEmpty() && includedPackages.contains(packageInclusion.key.name)) {
                    continue@commandLoop
                }
            }

            // If none of the dependencies are included
            if (includedPackages.toSet().intersect(dependencies.map { it.name }).isEmpty()) {
                // We know dependencies is not empty
                val range = TextRange(0, latexCommands.minBy { 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.ERROR,
                        isOntheFly,
                        *fixes
                ))
            }
        }
    }

    /**
     * @author Hannah Schellekens
     */
    private class ImportCommandFix(val pack: nl.hannahsten.texifyidea.lang.Package) : LocalQuickFix {

        override fun getFamilyName() = "Add import for package '${pack.name}'"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val file = command.containingFile

            if (!PackageUtils.insertUsepackage(file, pack)) {
                Notification("LatexMissingImportInspection", "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'"

        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

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.PsiElement
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.intentions.LatexAddLabelIntention
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.settings.TexifyConfigurable
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.environmentsInFile
import nl.hannahsten.texifyidea.util.files.openedEditor
import org.jetbrains.annotations.Nls
import java.util.EnumSet

/**
 * 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 minimumLevel = Magic.Command.labeledLevels[TexifySettings.getInstance().missingLabelMinimumLevel] ?: error("No valid minimum level given")
        val labeledCommands = Magic.Command.labeledLevels.keys.filter { command ->
            Magic.Command.labeledLevels[command]?.let { it <= minimumLevel } == true // -1 is a higher level than 0
        }.map { "\\" + it.command }
        file.commandsInFile().filter {
            labeledCommands.contains(it.name) && it.name != "\\item" && !it.hasStar()
        }.forEach { addCommandDescriptor(it, descriptors, manager, isOntheFly) }

        file.environmentsInFile().filter { Magic.Environment.labeled.containsKey(it.environmentName) }
            .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
        }

        // For adding the label, see LatexAddLabelIntention
        descriptors.add(manager.createProblemDescriptor(
                command,
                "Missing label",
                arrayOf(InsertLabelAfterCommandFix(), ChangeMinimumLabelLevelFix()),
                ProblemHighlightType.WEAK_WARNING,
                isOntheFly,
                false
        ))

        return true
    }

    private fun addEnvironmentDescriptor(environment: LatexEnvironment, descriptors: MutableList<ProblemDescriptor>,
                                         manager: InspectionManager, isOntheFly: Boolean): Boolean {
        if (environment.label != null) {
            return false
        }

        descriptors.add(manager.createProblemDescriptor(
                environment,
                "Missing label",
                arrayOf(InsertLabelInEnvironmentFix()),
                ProblemHighlightType.WEAK_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 minimum sectioning level"
        }

        override fun startInWriteAction() = false

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            ShowSettingsUtil.getInstance().showSettingsDialog(project, TexifyConfigurable::class.java)
        }
    }

    abstract class LabelQuickFix : LocalQuickFix {
        protected fun getUniqueLabelName(base: String, prefix: String?, file: PsiFile): String {
            val labelBase = "$prefix:$base"
            val allLabels = file.findLatexAndBibtexLabelStringsInFileSet()
            return appendCounter(labelBase, allLabels)
        }

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

    /**
     * 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 InsertLabelAfterCommandFix : LabelQuickFix() {

        // 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
            LatexAddLabelIntention().invoke(project, command.containingFile.openedEditor(), command.containingFile)
        }
    }

    private class InsertLabelInEnvironmentFix : LabelQuickFix() {
        override fun getFamilyName() = "Add label for this environment"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexEnvironment
            val helper = LatexPsiHelper(project)
            // Determine label name.
            val createdLabel = getUniqueLabelName(command.environmentName.formatAsLabel(),
                    Magic.Environment.labeled[command.environmentName], command.containingFile)

            val moveCaretAfter: PsiElement
            moveCaretAfter = if (Magic.Environment.labelAsParameter.contains(command.environmentName)) {
                val insertedElements = helper.addOptionalParameter(command.beginCommand, "label", createdLabel)
                insertedElements.last()
            }
            else {
                // in a float environment the label must be inserted after a caption
                val labelCommand = helper.addToContent(command, helper.createLabelCommand(createdLabel),
                        command.environmentContent?.childrenOfType<LatexCommands>()
                                ?.findLast { c -> c.name == "\\caption" })
                labelCommand
            }

            // Adjust caret offset
            val openedEditor = command.containingFile.openedEditor() ?: return
            val caretModel = openedEditor.caretModel
            caretModel.moveToOffset(moveCaretAfter.endOffset())
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.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,
                        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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexRequiredParam
import nl.hannahsten.texifyidea.util.PackageUtils
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.firstChildOfType
import nl.hannahsten.texifyidea.util.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 = PackageUtils.getIncludedPackagesList(file)
        val covered = HashSet<String>()
        val duplicates = HashSet<String>()
        packages.filterNotTo(duplicates) {
            covered.add(it)
        }

        // Duplicates!
        file.commandsInFile().asSequence()
                .filter { it.name == "\\usepackage" && 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,
                            isOntheFly
                    ))
                }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.insight.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.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,
                            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

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.insight.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.Magic
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
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 Magic.Command.illegalExtensions }
                .filter { command ->
                    Magic.Command.illegalExtensions[command.name]!!.any {
                        extension -> command.requiredParameters.any { it?.split(",")?.any { parameter -> parameter.endsWith(extension) } == true }
                    }
                }
                .forEach { command ->
                    val parameterList = command.requiredParameters.map { it.split(",") }.flatten()
                    var offset = 0
                    for (parameter in parameterList) {
                        if (Magic.Command.illegalExtensions[command.name]!!.any { parameter.endsWith(it) }) {
                            descriptors.add(manager.createProblemDescriptor(
                                    command,
                                    TextRange(offset, offset + parameter.length).shiftRight(command.commandToken.textLength + 1),
                                    "File argument should not include the extension",
                                    ProblemHighlightType.GENERIC_ERROR,
                                    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.requiredParameters.map { it.split(",") }.flatten()
            var offset = 0
            for (parameter in parameterList) {
                if (Magic.Command.illegalExtensions[command.name]!!.any { parameter.endsWith(it) }) {
                    val range = TextRange(offset, offset + parameter.length).shiftRight(command.parameterList.first { it.requiredParam != null }.textOffset + 1)
                    val replacement = Magic.Command.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

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 nl.hannahsten.texifyidea.insight.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.LatexContent
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.childrenOfType
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.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"
        )
    }

    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 commands = file.commandsInFile()
        for (command in commands) {
            // Only target references.
            if (!Magic.Command.reference.contains(command.name)) continue

            // Don't consider certain commands.
            if (command.name in IGNORED_COMMANDS) continue

            // Get the NORMAL_TEXT in front of the command.
            val sibling = command.parent?.prevSibling ?: command.parentOfType(LatexContent::class)?.prevSibling ?: continue

            // When sibling is whitespace, it's obviously bad news.
            if (sibling is PsiWhiteSpace) {
                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 (Magic.Pattern.endsWithNonBreakingSpace.matcher(text.text).find()) {
                    replacement = ""
                }
            }

            // Otherwise, just replace all the whitespace by a tilde.
            document.replaceString(offset, offset + whitespace.textLength, replacement)
        }
    }

    /**
     * Replaces the ending of [LatexNormalText] element with `~`.
     *
     * @author Hannah Schellekens
     */
    private class TextReplacementFix : LocalQuickFix {

        override fun getFamilyName() = "Insert non-breaking space"

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            val command = descriptor.psiElement as LatexCommands
            val file = command.containingFile
            val document = file.document() ?: return

            document.insertString(command.textOffset, "~")
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.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.*
import nl.hannahsten.texifyidea.util.files.document
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,
                    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,
                    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

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.matches
import nl.hannahsten.texifyidea.util.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 Magic.Command.endIfs) {
                // Non-opened fi.
                if (stack.isEmpty()) {
                    descriptors.add(manager.createProblemDescriptor(
                            command,
                            "No matching \\if-command found",
                            Magic.General.noQuickFix,
                            ProblemHighlightType.GENERIC_ERROR,
                            isOntheFly
                    ))
                    continue
                }

                stack.pop()
            }
            else if (Magic.Pattern.ifCommand.matches(name) && name !in Magic.Command.ignoredIfs && command.previousCommand()?.name != "\\newif") {
                stack.push(command)
            }
        }

        // Mark unclosed ifs.
        for (cmd in stack) {
            descriptors.add(manager.createProblemDescriptor(
                    cmd,
                    "If statement is not closed",
                    Magic.General.noQuickFix,
                    ProblemHighlightType.GENERIC_ERROR,
                    isOntheFly
            ))
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexMathContent
import nl.hannahsten.texifyidea.psi.LatexPsiUtil.getNextSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.psi.LatexPsiUtil.getPreviousSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.util.deleteElement
import org.jetbrains.annotations.Nls
import java.util.EnumSet

/**
 * @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.
            var content = element.parent.parent
            if (content is LatexMathContent) {
                content = element.parent
            }
            val previous = getPreviousSiblingIgnoreWhitespace(content!!)
            val next = getNextSiblingIgnoreWhitespace(content)
            val before = if (previous == null) "" else previous.text
            val after = if (next == null) "" else next.text
            val replacement = String.format("\\frac{%s}{%s}", before, after)
            val document =
                PsiDocumentManager.getInstance(project).getDocument(element.containingFile)

            // Delete denominator.
            if (next != null) {
                document!!.deleteElement(next)
            }

            // Replace command.
            val range = element.commandToken.textRange
            document?.replaceString(range.startOffset, range.endOffset, replacement)

            // Replace numerator.
            if (previous != null) {
                document!!.deleteElement(previous)
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.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.toLowerCase() }
        val customPackages = LatexDefinitionIndex.getCommandsByName("\\ProvidesPackage", file.project, file.project.projectSearchScope)
                .map { it.requiredParameter(0) }
                .map { it?.toLowerCase() }
        val packages = ctanPackages + customPackages

        val commands = file.childrenOfType(LatexCommands::class)
                .filter { it.name == "\\usepackage" || it.name == "\\RequirePackage" }

        for (command in commands) {
            val `package` = command.requiredParameters.firstOrNull()?.toLowerCase()
            if (!packages.contains(`package`)) {
                descriptors.add(manager.createProblemDescriptor(
                        command,
                        displayName,
                        Magic.General.noQuickFix,
                        ProblemHighlightType.WARNING,
                        isOntheFly
                ))
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.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.requiredParameters.first().split("/").last()
            val fileName = file.name.removeSuffix(".sty")
            if (fileName != providesName) {
                descriptors.add(manager.createProblemDescriptor(
                        command,
                        displayName,
                        PackageNameMatchFileNameQuickFix,
                        ProblemHighlightType.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.requiredParameters.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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.run.latex.LatexDistribution
import nl.hannahsten.texifyidea.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 isEnabledByDefault(): Boolean {
        return LatexDistribution.isTexliveAvailable
    }

    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 (LatexDistribution.isTexliveAvailable && SystemEnvironment.isTlmgrInstalled) {
            val installedPackages = TexLivePackages.packageList
            val customPackages = LatexDefinitionIndex.getCommandsByName("\\ProvidesPackage", file.project, file.project
                            .projectSearchScope)
                    .map { it.requiredParameter(0) }
                    .mapNotNull { it?.toLowerCase() }
            val packages = installedPackages + customPackages

            val commands = file.childrenOfType(LatexCommands::class)
                    .filter { it.name == "\\usepackage" || it.name == "\\RequirePackage" }

            for (command in commands) {
                val `package` = command.requiredParameters.firstOrNull()?.toLowerCase() ?: continue
                if (`package` !in packages) {
                    // Use the cache or manually check if the package is installed (e.g. rubikrotation is listed as rubik, so we need to check it separately).
                    if (knownNotInstalledPackages.contains(`package`) || "tlmgr search --file /$`package`.sty".runCommand()
                                    ?.isEmpty() == true) {
                        descriptors.add(manager.createProblemDescriptor(
                                command,
                                "Package is not installed or \\ProvidesPackage is missing",
                                InstallPackage(SmartPointerManager.getInstance(file.project).createSmartPsiElementPointer(file), `package`, knownNotInstalledPackages),
                                ProblemHighlightType.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()
            ProgressManager.getInstance()
                    .run(object : Task.Backgroundable(project, "Installing $packageName...") {
                        override fun run(indicator: ProgressIndicator) {
                            val tlname = TexLivePackages.findTexLiveName(this, packageName)

                            if (tlname == null) {
                                Notification(
                                        "Package Not Installed",
                                        "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 = "tlmgr install $tlname".runCommand()
                            val update = "tlmgr update --self"
                            if (output?.contains(update) == true) {
                                title = "Updating tlmgr..."
                                update.runCommand()
                                title = "Installing $packageName..."
                                "tlmgr 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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexPsiHelper
import nl.hannahsten.texifyidea.util.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.requiredParameters.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.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

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

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.index.LatexCommandsIndex
import nl.hannahsten.texifyidea.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexMathContent
import nl.hannahsten.texifyidea.psi.LatexPsiUtil.getNextSiblingIgnoreWhitespace
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.deleteElement
import org.jetbrains.annotations.Nls
import kotlin.reflect.jvm.internal.impl.utils.SmartList

/**
 * @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: MutableList<ProblemDescriptor> =
            SmartList()
        val commands = LatexCommandsIndex.getItems(file)
        for (command in commands) {
            val index = Magic.Command.stylePrimitives.indexOf(command.name)
            if (index < 0) {
                continue
            }
            descriptors.add(
                manager.createProblemDescriptor(
                    command,
                    "Use of TeX primitive " + Magic.Command.stylePrimitives[index] + " is discouraged",
                    InspectionFix(),
                    ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                    isOntheFly
                )
            )
        }
        return descriptors
    }

    private inner class InspectionFix : LocalQuickFix {
        @Nls
        override fun getFamilyName(): String {
            return "Convert to LaTeX alternative"
        }

        override fun applyFix(
            project: Project,
            descriptor: ProblemDescriptor
        ) {
            val element = descriptor.psiElement as? LatexCommands ?: return

            // Find elements that go after the primitive.
            val cmdIndex = Magic.Command.stylePrimitives.indexOf(element.name)
            if (cmdIndex < 0) {
                return
            }
            var content = element.parent.parent
            if (content is LatexMathContent) {
                content = element.parent
            }
            val next = getNextSiblingIgnoreWhitespace(content!!)
            val after = if (next == null) "" else next.text
            val replacement =
                String.format(Magic.Command.stylePrimitveReplacements[cmdIndex], after)
            val document =
                PsiDocumentManager.getInstance(project).getDocument(element.containingFile)

            // Delete the ending part..
            if (next != null) {
                document!!.deleteElement(next)
            }

            // Replace command.
            val range = element.commandToken.textRange
            document?.replaceString(range.startOffset, range.endOffset, replacement)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.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.childrenOfType
import nl.hannahsten.texifyidea.util.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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.lang.Diacritic
import nl.hannahsten.texifyidea.lang.magic.MagicCommentScope
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexContent
import nl.hannahsten.texifyidea.psi.LatexMathEnvironment
import nl.hannahsten.texifyidea.util.allCommands
import java.text.Normalizer
import java.util.*

/**
 * @author Sten Wessel
 */
open class LatexRedundantEscapeInspection : TexifyInspectionBase() {

    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.requiredParameters.isNotEmpty() || hasNormalTextSibling(command))) {
                descriptors.add(manager.createProblemDescriptor(
                        command,
                        "Redundant diacritic escape",
                        RemoveEscapeFix(),
                        ProblemHighlightType.WEAK_WARNING,
                        isOntheFly
                ))
            }
        }

        return descriptors
    }

    private fun hasNormalTextSibling(command: LatexCommands): Boolean {
        val content = PsiTreeUtil.getParentOfType(command, LatexContent::class.java)
        val siblingContent = PsiTreeUtil.getNextSiblingOfType(content, LatexContent::class.java)
        return siblingContent?.noMathContent?.normalText != null
    }

    /**
     * @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.requiredParameters.getOrNull(0)
            if (param != null) {
                // Just a required parameter
                range = command.textRange
                base = param
            }
            else {
                // Now find a sibling
                val content = PsiTreeUtil.getParentOfType(command, LatexContent::class.java)
                val siblingContent = PsiTreeUtil.getNextSiblingOfType(content, LatexContent::class.java)
                val siblingText = siblingContent?.noMathContent?.normalText

                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

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) = if (it.group(3) != null) {
            it.groupRange(3)
        }
        else if (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

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.insight.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.Magic
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.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 Magic.Command.requiredExtensions }
                .filter { command ->
                    Magic.Command.requiredExtensions[command.name]!!.any {
                        extension -> command.requiredParameters.any { it?.split(",")?.any { parameter -> parameter.endsWith(extension) } == false }
                    }
                }
                .forEach { command ->
                    val parameterList = command.requiredParameters.map { it.split(",") }.flatten()
                    var offset = 0
                    for (parameter in parameterList) {
                        if (Magic.Command.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,
                                    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.requiredParameters.map { it.split(",") }.flatten()
            var offset = 0
            for (parameter in parameterList) {
                if (Magic.Command.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 = Magic.Command.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

import com.intellij.psi.PsiElement
import nl.hannahsten.texifyidea.inspections.TexifyRegexInspection
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.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

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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.childrenOfType
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.inMathContext

/**
 * @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 = Magic.Pattern.comments.matcher(text.text)
            val commentParts = arrayListOf<IntRange>()
            while (commentMatcher.find()) {
                commentParts.add(commentMatcher.start()..commentMatcher.end())
            }

            val matcher = Magic.Pattern.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.WEAK_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
     */
    private open class NormalSpaceFix(val whitespaceRange: IntRange) : LocalQuickFix {

        override fun getFamilyName() = "Insert normal space"

        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.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.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.insight.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.LatexEndCommand
import nl.hannahsten.texifyidea.psi.LatexPsiUtil
import nl.hannahsten.texifyidea.ui.CreateFileDialog
import nl.hannahsten.texifyidea.util.*
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.createFile
import nl.hannahsten.texifyidea.util.files.findRootFile
import org.intellij.lang.annotations.Language
import java.io.File
import java.util.EnumSet
import java.util.regex.Pattern

/**
 * @author Hannah Schellekens
 */
open class LatexTooLargeSectionInspection : TexifyInspectionBase() {

    companion object {

        @Language("RegExp")
        private val SECTION_COMMAND = Pattern.compile("\\\\(section|chapter)\\{[^{}]+}")

        /**
         * All commands that count as inspected sections in order of hierarchy.
         */
        private val SECTION_NAMES = listOf("\\chapter", "\\section")

        /**
         * The amount of characters it takes before a section is considered 'too long'.
         */
        private const val TOO_LONG_LIMIT = 4000

        /**
         * 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.
         */
        fun findNextSection(command: LatexCommands): PsiElement? {
            // Scan all commands.
            val commands = command.containingFile.commandsInFile().toList()

            for (i in commands.indices) {
                val cmd = commands[i]

                val indexOfCurrent = SECTION_NAMES.indexOf(cmd.name)
                if (indexOfCurrent < 0) {
                    continue
                }

                if (cmd == command && i + 1 < commands.size) {
                    val next = commands[i + 1]

                    val indexOfNext = SECTION_NAMES.indexOf(next.name)
                    if (indexOfNext in 0..indexOfCurrent) {
                        return commands[i + 1]
                    }
                }
            }

            // If no command was found, find the end of the document.
            val children = command.containingFile.childrenOfType(LatexEndCommand::class)
            return if (children.isEmpty()) null else children.last()
        }
    }

    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) }

        if (isAlreadySplit(commands)) {
            return descriptors
        }

        for (i in commands.indices) {
            if (!isTooLong(commands[i], findNextSection(commands[i]))) {
                continue
            }

            descriptors.add(
                    manager.createProblemDescriptor(
                            commands[i],
                            "Section is long and may be moved to a separate file.",
                            InspectionFix(),
                            ProblemHighlightType.WEAK_WARNING,
                            isOntheFly
                    )
            )
        }

        return descriptors
    }

    /**
     * Checks if the given file is already a split up section.
     *
     * Not the best way perhaps, but it does the job.
     */
    private fun isAlreadySplit(commands: Collection<LatexCommands>): Boolean {
        val smallestIndex = commands.asSequence()
                .map { cmd -> SECTION_NAMES.indexOf(cmd.name) }
                .min() ?: return false

        // Just check if \section or \chapter occur only once.
        for (name in SECTION_NAMES) {
            if (SECTION_NAMES.indexOf(name) > smallestIndex) {
                continue
            }

            if (commands.asSequence().filter { cmd -> cmd.name == name }.count() > 1) {
                return false
            }
        }

        return true
    }

    /**
     * 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 (see [TOO_LONG_LIMIT])
     */
    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

        return (endIndex - startIndex) >= TOO_LONG_LIMIT
    }

    /**
     * @author Hannah Schellekens
     */
    class InspectionFix : LocalQuickFix {

        companion object {

            /**
             * Finds the label command of the given command.
             */
            fun findLabel(cmd: LatexCommands): LatexCommands? {
                val grandparent = cmd.parent.parent
                val sibling = LatexPsiUtil.getNextSiblingIgnoreWhitespace(grandparent) ?: return null
                val child = sibling.firstChildOfType(LatexCommands::class) ?: return null
                return if (child.name == "\\label") child 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.size > 0) 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 createdFile = createFile("$filePath.tex", text)
                document.deleteString(startIndex, endIndex)
                LocalFileSystem.getInstance().refresh(true)
                val fileNameRelativeToRoot = createdFile.absolutePath
                        .replace(File.separator, "/")
                        .replace("$root/", "")
                val indent = cmd.findIndentation()
                document.insertString(startIndex, "\n$indent\\input{${fileNameRelativeToRoot.dropLast(4)}}\n\n")
            }
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.insight.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.Magic
import nl.hannahsten.texifyidea.util.endOffset
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.files.document
import nl.hannahsten.texifyidea.util.firstChildOfType
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 Magic.Command.sectionMarkers) {
                continue
            }

            val sectionName = command.firstChildOfType(LatexRequiredParam::class)?.text?.trimRange(1, 1) ?: continue
            if (!Magic.Pattern.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.WEAK_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

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.options.ShowSettingsUtil
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.insight.InsightGroup
import nl.hannahsten.texifyidea.inspections.TexifyInspectionBase
import nl.hannahsten.texifyidea.inspections.latex.LatexUnicodeInspection.EscapeUnicodeFix
import nl.hannahsten.texifyidea.inspections.latex.LatexUnicodeInspection.InsertUnicodePackageFix
import nl.hannahsten.texifyidea.lang.Diacritic
import nl.hannahsten.texifyidea.lang.LatexCommand
import nl.hannahsten.texifyidea.lang.LatexMathCommand
import nl.hannahsten.texifyidea.lang.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.TexifyProjectConfigurable
import nl.hannahsten.texifyidea.settings.TexifyProjectSettings
import nl.hannahsten.texifyidea.run.latex.LatexDistribution
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.PackageUtils
import nl.hannahsten.texifyidea.util.insertUsepackage
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 = TexifyProjectSettings.getInstance(file.project).compilerCompatibility
            if (compilerCompat == LatexCompiler.LUALATEX || compilerCompat == LatexCompiler.XELATEX || LatexDistribution.texliveVersion >= 2018) {
                return true
            }

            val included = PackageUtils.getIncludedPackages(file)
            return Magic.Package.unicode.stream().allMatch { p -> included.contains(p.name) }
        }
    }

    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 = Magic.Pattern.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
                }

                descriptors.add(manager.createProblemDescriptor(
                        text,
                        TextRange(matcher.start(), matcher.end()),
                        "Unsupported non-ASCII character",
                        ProblemHighlightType.ERROR,
                        isOntheFly,
                        EscapeUnicodeFix(inMathMode),
                        if (inMathMode) {
                            null
                        }
                        else {
                            InsertUnicodePackageFix()
                        },
                        ChangeCompilerCompatibilityFix()
                ))
            }
        }

        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

            Magic.Package.unicode.forEach { p ->
                file.insertUsepackage(p)
            }
        }
    }

    /**
     * Open the settings page so the user can change the compiler compability.
     */
    private class ChangeCompilerCompatibilityFix : LocalQuickFix {
        @Nls
        override fun getFamilyName(): String {
            return "Change compiler compatibility"
        }

        // We just want to open the settings window, but cannot do an AWT event inside a write action
        override fun startInWriteAction() = false

        override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
            ShowSettingsUtil.getInstance().showSettingsDialog(project, TexifyProjectConfigurable::class.java)
        }
    }

    /**
     * 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 [LatexMathCommand] (where appropiate). 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 internal constructor(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 c = descriptor.textRangeInElement.substring(element.text)

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

            // When no replacement is found, show error message
            val document = PsiDocumentManager.getInstance(project).getDocument(element.containingFile)
            if (replacement == null) {
                val editor = FileEditorManager.getInstance(project).selectedTextEditor
                if (editor != null) {
                    HintManager.getInstance().showErrorHint(editor, "Character could not be converted")
                }
                return
            }

            // Fill in replacement
            val range = descriptor.textRangeInElement.shiftRight(element.textOffset)
            document?.replaceString(range.startOffset, range.endOffset, 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] }
                    .map { if (inMathMode)
                        Diacritic.Math.fromUnicode(it) as Diacritic
                    else
                        Diacritic.Normal.fromUnicode(it) }

            return Diacritic.buildChain(base, diacritics)
        }
    }
}

package nl.hannahsten.texifyidea.inspections.latex

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.insight.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.Magic
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.findLatexAndBibtexLabelStringsInFileSet
import nl.hannahsten.texifyidea.util.firstParentOfType
import java.util.EnumSet

/**
 * @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 (!Magic.Command.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 Magic.Command.commandDefinitions) {
                continue
            }

            val required = command.requiredParameters
            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.sumBy { it.length + 1 }
                    for (j in 0 until i) {
                        offset += parts[j].length + 1
                    }

                    descriptors.add(manager.createProblemDescriptor(
                            command,
                            TextRange.from(offset, part.length),
                            "Unresolved reference '$part'",
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                            isOntheFly
                    ))
                }
            }
        }

        return descriptors
    }
}

package nl.hannahsten.texifyidea.inspections.latex

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.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());

    override fun toString(): String = key
}

@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 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 a non PsiComment is found or null (the scan ends at the first non-comment).
    while (current is PsiComment) {

        // Only consider magic comments.
        val commentText = current.text
        current = current.next()
        if (TextBasedMagicCommentParser.COMMENT_PREFIX.containsMatchIn(commentText).not()) continue

        // 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::nextSiblingIgnoreWhitespace,
        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> {
    return forwardMagicCommentLookup { firstChildIgnoringWhitespaceOrNull() }
}

/**
 * Get the magic comment that directly precedes the environment.
 */
fun LatexEnvironment.magicComment(): MagicComment<String, String> {
    val outerContent = parentOfType(LatexContent::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(LatexContent::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> {
    // Because of current parser quirks, there are two scenarios to find the magic comments:
    // 1. When the magic comments are the first elements in an environment, they are next siblings of
    //    the LatexBeginCommand of the parent LatexEnvironment.
    // 2. Otherwise they are previous siblings of the direct LatexContent parent of the LatexCommands.

    val directParentContent = parentOfType(LatexContent::class) ?: return MagicComment.empty()
    val parentContentPreviousSibling = directParentContent.previousSiblingIgnoreWhitespace()

    // Case 2.
    if (parentContentPreviousSibling is PsiComment) {
        return backwardMagicCommentLookup { parentContentPreviousSibling }
    }

    // Case 1.
    val parentEnvironment = parentOfType(LatexEnvironment::class) ?: return MagicComment.empty()
    val beginCommand = parentEnvironment.firstChildIgnoringWhitespaceOrNull() ?: return MagicComment.empty()
    return forwardMagicCommentLookup { beginCommand.nextSiblingIgnoreWhitespace() }
}

/**
 * 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.magic.TextBasedMagicCommentParser.Companion.COMMENT_PREFIX

/**
 * 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.
            if (keyMatcher.find()) {
                // Register previous key/value pair.
                if (key != null) {
                    pushKeyValuePair()
                }

                key = keyMatcher.group(1).toLowerCase()

                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.
            else if (key == null) {
                addValue(line.split(" ").first().asKey(), 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.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)

        // 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.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 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)

        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 ?: "" })
        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.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.ComboBox
import com.intellij.openapi.ui.LabeledComponent
import com.intellij.openapi.ui.TextBrowseFolderListener
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.VerticalFlowLayout
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 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 ?: ""
        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.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)

            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 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(it.split("""\s+""".toRegex())) }

        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 nl.hannahsten.texifyidea.run.bibtex.BibtexRunConfiguration
import nl.hannahsten.texifyidea.run.latex.LatexDistribution

/**
 * @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(it.split("""\s+""".toRegex())) }

            // Include files from auxiliary directory on Windows
            if (LatexDistribution.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.GeneralCommandLine
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.run.latex.LatexDistributionType
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import nl.hannahsten.texifyidea.util.LatexmkRcFileFinder
import nl.hannahsten.texifyidea.util.runCommand
import nl.hannahsten.texifyidea.util.splitWhitespace

/**
 * @author Hannah Schellekens, Sten Wessel
 */
@Suppress("unused", "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> {
            val command = mutableListOf(runConfig.compilerPath ?: "pdflatex")

            command.add("-file-line-error")
            command.add("-interaction=nonstopmode")
            command.add("-synctex=1")
            command.add("-output-format=${runConfig.outputFormat.name.toLowerCase()}")

            command.add("-output-directory=$outputPath")

            // -aux-directory only exists on MiKTeX
            if (auxilPath != null && runConfig.latexDistribution.isMiktex()) {
                command.add("-aux-directory=$auxilPath")
            }

            // Prepend root paths to the input search path
            if (runConfig.latexDistribution.isMiktex()) {
                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 ?: "lualatex")

            // 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.toLowerCase()}")

            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 ?: "latexmk")

            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.toLowerCase()}")
            }

            command.add("-output-directory=$outputPath")

            if (auxilPath != null && runConfig.latexDistribution.isMiktex()) {
                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 ?: "xelatex")

            // 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.latexDistribution.isMiktex()) {
                command.add("-aux-directory=$auxilPath")
            }

            // Prepend root paths to the input search path
            if (runConfig.latexDistribution.isMiktex()) {
                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 ?: "texliveonfly")

            // 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 ?: "tectonic")

            command.add("--synctex")

            command.add("--outfmt=${runConfig.outputFormat.name.toLowerCase()}")

            if (outputPath != null) {
                command.add("--outdir=$outputPath")
            }

            return command
        }
    };

    /**
     * Convert Windows paths to WSL paths.
     */
    private fun String.toPath(runConfig: LatexRunConfiguration): String =
        if (runConfig.latexDistribution == LatexDistributionType.WSL_TEXLIVE) {
            "wsl wslpath -a '$this'".runCommand() ?: 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.latexDistribution != 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.latexDistribution != LatexDistributionType.DOCKER_MIKTEX) {
            runConfig.outputPath.getAndCreatePath()?.path?.toPath(runConfig)
        }
        else {
            dockerOutputDir
        }

        val auxilPath = if (runConfig.latexDistribution != LatexDistributionType.DOCKER_MIKTEX) {
            runConfig.auxilPath?.path?.toPath(runConfig)
        }
        else {
            dockerAuxilDir
        }

        var command = createCommand(
            runConfig,
            auxilPath,
            outputPath,
            moduleRoot,
            moduleRoots
        )

        if (runConfig.latexDistribution == LatexDistributionType.WSL_TEXLIVE) {
            command = mutableListOf("bash", "-ic", GeneralCommandLine(command).commandLineString)
        }

        if (runConfig.latexDistribution == LatexDistributionType.DOCKER_MIKTEX) {
            createDockerCommand(runConfig, dockerAuxilDir, dockerOutputDir, mainFile, command)
        }

        // Custom compiler arguments specified by the user
        runConfig.compilerArguments?.let { arguments ->
            arguments.splitWhitespace()
                .dropLastWhile { it.isEmpty() }
                .forEach { command.add(it) }
        }

        if (runConfig.latexDistribution == LatexDistributionType.WSL_TEXLIVE) {
            command[command.size - 1] = command.last() + " ${mainFile.path.toPath(runConfig)}"
        }
        else {
            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()

        val parameterList = mutableListOf(
                "docker",
                "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 == null) {
            runConfig.setDefaultAuxilPath()
        }

        if (runConfig.auxilPath != null && runConfig.auxilPath != mainFile.parent) {
            parameterList.addAll(listOf("-v", "${runConfig.auxilPath?.path}:$dockerAuxilDir"))
        }

        parameterList.add("docker.pkg.github.com/hannah-sten/texify-idea/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.util.SystemInfo
import com.intellij.openapi.vfs.VirtualFile
import nl.hannahsten.texifyidea.editor.autocompile.AutoCompileDoneListener
import nl.hannahsten.texifyidea.lang.LatexRegularCommand
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.linuxpdfviewer.PdfViewer
import nl.hannahsten.texifyidea.run.linuxpdfviewer.ViewerForwardSearch
import nl.hannahsten.texifyidea.run.makeindex.RunMakeindexListener
import nl.hannahsten.texifyidea.run.sumatra.SumatraForwardSearchListener
import nl.hannahsten.texifyidea.run.sumatra.isSumatraAvailable
import nl.hannahsten.texifyidea.settings.TexifySettings
import nl.hannahsten.texifyidea.util.Magic.Package
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.includedPackages
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()
        }

        firstRunSetup(compiler)
        runConfig.outputPath.updateOutputSubDirs()

        val handler = createHandler(mainFile, compiler)
        val isMakeindexNeeded = runMakeindexIfNeeded(handler, mainFile, runConfig.filesToCleanUp)
        runConfig.hasBeenRun = true

        if (!isLastCompile(isMakeindexNeeded, handler)) return handler
        scheduleBibtexRunIfNeeded(handler)
        schedulePdfViewerIfNeeded(handler)
        scheduleFileCleanup(runConfig.filesToCleanUp, 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)
                .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
    }

    private fun firstRunSetup(compiler: LatexCompiler) {
        // Some initial setup
        if (!runConfig.hasBeenRun) {
            // 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) {
                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.bibWorkingDir = runConfig.getAuxilDirectory()
                    }
                }
            }
        }
    }

    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 = "\\" + LatexRegularCommand.MAKENOIDXGLOSSARIES.command 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(Package.index + Package.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("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) {
            if (!runConfig.compileTwice) {
                runConfig.isLastRunConfig = true
            }

            // Schedule the second compile only if this is the first compile
            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>, handler: KillableProcessHandler) {
        if (runConfig.isLastRunConfig) {
            handler.addProcessListener(FileCleanupListener(filesToCleanUp))
        }
    }

    /**
     * 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 (!containsPlaceholder) {
                // If no placeholder was used, assume the path is the final argument
                commandList += runConfig.outputFilePath
            }

            handler.addProcessListener(OpenCustomPdfViewerListener(commandList.toTypedArray(), runConfig = runConfig))
        }
        else if (runConfig.sumatraPath != null || isSumatraAvailable) {
            // Open Sumatra after compilation & execute inverse search.
            handler.addProcessListener(SumatraForwardSearchListener(runConfig, environment))
        }
        else if (TexifySettings.getInstance().pdfViewer in listOf(PdfViewer.EVINCE, PdfViewer.OKULAR, PdfViewer.ZATHURA, PdfViewer.SKIM)) {
            ViewerForwardSearch(TexifySettings.getInstance().pdfViewer).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.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()
            setDefaultOutputFormat()
            setSuggestedName()
            setDefaultDistribution()
        }
        is BibtexRunConfigurationType -> BibtexRunConfiguration(project, this, "BibTeX")
        is MakeindexRunConfigurationType -> MakeindexRunConfiguration(project, this, "Makeindex")
        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.util.SystemInfo
import nl.hannahsten.texifyidea.util.runCommand

/**
 * Represents the LaTeX Distribution of the user, e.g. MikTeX or TeX Live.
 */
class LatexDistribution {

    companion object {

        private val pdflatexVersionText: String by lazy {
            getDistribution()
        }

        private val dockerImagesText: String by lazy {
            runCommand("docker", "image", "ls")
        }

        /**
         * Guess the LaTeX distribution that the user probably is using / wants to use.
         */
        val defaultLatexDistribution: LatexDistributionType by lazy {
            when {
                isMiktexAvailable -> LatexDistributionType.MIKTEX
                isTexliveAvailable -> LatexDistributionType.TEXLIVE
                defaultIsDockerMiktex() -> LatexDistributionType.DOCKER_MIKTEX
                else -> LatexDistributionType.TEXLIVE
            }
        }

        /**
         * 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")
        }

        /**
         * Whether the user is using TeX Live or not.
         * This value is only computed once.
         */
        val isTexliveAvailable: Boolean by lazy {
            pdflatexVersionText.contains("TeX Live")
        }

        private val isDockerMiktexAvailable: Boolean by lazy {
            dockerImagesText.contains("miktex")
        }

        private val isWslTexliveAvailable: Boolean by lazy {
            SystemInfo.isWindows && runCommand("bash", "-ic", "pdflatex --version").contains("pdfTeX")
        }

        /**
         * 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 && !isTexliveAvailable && dockerImagesText.contains("miktex"))

        fun isInstalled(type: LatexDistributionType): Boolean {
            if (type == LatexDistributionType.MIKTEX && isMiktexAvailable) return true
            if (type == LatexDistributionType.TEXLIVE && isTexliveAvailable) return true
            if (type == LatexDistributionType.DOCKER_MIKTEX && isDockerMiktexAvailable) return true
            if (type == LatexDistributionType.WSL_TEXLIVE && isWslTexliveAvailable) return true
            return false
        }

        /**
         * Returns year of texlive installation, 0 if it is not texlive.
         * Assumes the pdflatex version output contains something like (TeX Live 2019).
         */
        val texliveVersion: Int by lazy {
            if (!isTexliveAvailable) {
                0
            }
            else {
                val startIndex = pdflatexVersionText.indexOf("TeX Live")
                try {
                    pdflatexVersionText.substring(startIndex + "TeX Live ".length, startIndex + "TeX Live ".length + "2019".length).toInt()
                }
                catch (e: NumberFormatException) {
                    0
                }
            }
        }

        /**
         * Find the full name of the distribution in use, e.g. TeX Live 2019.
         */
        private fun getDistribution(): String {
            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 {
                ""
            }
        }
    }
}

package nl.hannahsten.texifyidea.run.latex

/**
 * See [LatexDistribution].
 */
enum class LatexDistributionType(val displayName: String) {
    TEXLIVE("TeX Live"),
    MIKTEX("MiKTeX"),
    WSL_TEXLIVE("TeX Live using WSL"),
    DOCKER_MIKTEX("Dockerized MiKTeX");

    fun isMiktex() = this == MIKTEX || this == DOCKER_MIKTEX

    fun isTexlive() = this == TEXLIVE || this == WSL_TEXLIVE

    fun isInstalled() = LatexDistribution.isInstalled(this)

    override fun toString() = displayName

    companion object {
        fun valueOfIgnoreCase(value: String?): LatexDistributionType {
            return values().firstOrNull { it.name.equals(value, true) } ?: LatexDistribution.defaultLatexDistribution
        }
    }
}

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.IndexNotReadyException
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 com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.util.files.FileUtil
import nl.hannahsten.texifyidea.util.files.createExcludedDir
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.files.referencedFileSet
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)
    }

    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? {
        // Caching of the result
        return getPath().also {
            virtualFile = it
        }
    }

    private fun getPath(): VirtualFile? {
        // When the user modifies the run configuration template, then this variable will magically be replaced with the
        // path to the /bin folder of IntelliJ, without the setter being called.
        if (virtualFile?.path?.endsWith("/bin") == true) {
            virtualFile = null
        }

        if (virtualFile != null) {
            return virtualFile!!
        }
        else {
            val pathString = if (pathString.contains(projectDirString)) {
                if (contentRoot == null) return null
                pathString.replace(projectDirString, contentRoot?.path ?: "")
            }
            else {
                if (mainFile == null) return null
                pathString.replace(mainFileString, mainFile?.parent?.path ?: "")
            }
            val path = LocalFileSystem.getInstance().findFileByPath(pathString)
            if (path != null && path.isDirectory) {
                return path
            }
            else {
                // Try to create the path
                // todo use createExcludedDir
                if (File(pathString).mkdirs()) {
                    LocalFileSystem.getInstance().refreshAndFindFileByPath(pathString)?.let {
                        return it
                    }
                }
            }
            // Path is invalid (perhaps the user provided an invalid path)
            Notification("LatexOutputPath", "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
                if (File(defaultPathString).mkdir()) {
                    LocalFileSystem.getInstance().refreshAndFindFileByPath(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!!)
            defaultOutputPath = LocalFileSystem.getInstance().findFileByPath(moduleRoot?.path + "/" + variant)
        }
        return defaultOutputPath
    }

    /**
     * Whether the current output path is the default.
     */
    fun isDefault() = getDefaultOutputPath() == virtualFile

    /**
     * Creates the output directories to place all produced files.
     */
    fun create() {
        val mainFile = mainFile ?: return

        val fileIndex = ProjectRootManager.getInstance(project).fileIndex

        val includeRoot = mainFile.parent
        val parentPath = fileIndex.getContentRootForFile(mainFile, false)?.path ?: includeRoot.path
        val outPath = "$parentPath/out"

        // Create output path for non-MiKTeX systems (MiKTeX creates it automatically)
        val module = fileIndex.getModuleForFile(mainFile, false)
        File(outPath).mkdirs()
        virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(outPath)
        module?.createExcludedDir(outPath)
    }

    /**
     * Copy subdirectories of the source directory to the output directory for includes to work in non-MiKTeX systems
     */
    @Throws(ExecutionException::class)
    fun updateOutputSubDirs() {
        val includeRoot = mainFile?.parent
        val outPath = virtualFile?.path ?: return

        val files: Set<PsiFile>
        try {
            files = mainFile?.psiFile(project)?.referencedFileSet() ?: emptySet()
        }
        catch (e: IndexNotReadyException) {
            throw ExecutionException("Please wait until the indices are built.", e)
        }

        // Create output paths (see issue #70 on GitHub)
        files.asSequence()
            .mapNotNull { FileUtil.pathRelativeTo(includeRoot?.path ?: return@mapNotNull null, it.virtualFile.parent.path) }
            .forEach { File(outPath + it).mkdirs() }
    }
}

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.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.util.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.hasBibliography
import nl.hannahsten.texifyidea.util.includedPackages
import nl.hannahsten.texifyidea.util.usesBiber
import org.jdom.Element
import java.io.File

/**
 * @author Hannah Schellekens, Sten Wessel
 */
class LatexRunConfiguration constructor(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 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 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()
        }

    // 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: VirtualFile? = null
        get() {
            // When the user modifies the run configuration template, then this variable will magically be replaced with the
            // path to the /bin folder of IntelliJ, without the setter being called.
            return if (field?.path?.endsWith("/bin") == true) {
                field = null
                field
            }
            else {
                field
            }
        }

    var compileTwice = false
    var outputFormat: Format = Format.PDF
    var latexDistribution: LatexDistributionType = LatexDistributionType.TEXLIVE

    /** 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)
            }
        }

    // 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")
        }
    }

    @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

        // 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)

        val fileSystem = LocalFileSystem.getInstance()

        // 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 = fileSystem.findFileByPath(auxilPathString)
        }

        // Backwards compatibility
        runReadAction {
            val auxDirBoolean = parent.getChildText(AUX_DIR)
            if (auxDirBoolean != null && this.auxilPath == 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 = 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()

        // Read makeindex run configurations
        val makeindexRunConfigElt = parent.getChildText(MAKEINDEX_RUN_CONFIG)
        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(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?.path ?: "" })
        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()

        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.toUpperCase())
            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
        }

        // On non-MiKTeX systems, override outputPath to disable the out/ directory by default for bibtex to work
        if (!latexDistribution.isMiktex()) {
            // Only if default, because the user could have changed it after creating the run config but before running
            if (outputPath.isDefault() && mainFile != null) {
                outputPath.virtualFile = mainFile!!.parent
            }
        }

        // When chapterbib is used, every chapter has its own bibliography and needs its own run config
        val usesChapterbib = psiFile?.includedPackages()?.contains("chapterbib") == true

        if (!usesChapterbib) {
            addBibRunConfig(defaultCompiler, mainFile, compilerFromMagicComment?.second)
        }
        else if (psiFile != null) {
            val allBibliographyCommands = psiFile!!.commandsInFileSet().filter { it.name == "\\bibliography" }

            // 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 == "\\include" }
                    .flatMap { command -> command.requiredParameters }
                    .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)
                        }
                    }
        }
    }

    /**
     * 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 setDefaultOutputFormat() {
        outputFormat = Format.PDF
    }

    fun setDefaultDistribution() {
        latexDistribution = LatexDistribution.defaultLatexDistribution
    }

    /**
     * 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 (auxilPath != null && latexDistribution.isMiktex() && auxilPath?.path?.endsWith("/bin") == false) {
            auxilPath
        }
        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()
                .toLowerCase()
    }

    /**
     * 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.
     */
    private fun getMainFileContentRoot(): VirtualFile? {
        if (mainFile == null) return null
        return runReadAction {
            return@runReadAction ProjectRootManager.getInstance(project).fileIndex.getContentRootForFile(mainFile!!)
        }
    }

    // todo refactor following methods in to LatexOutputPath
    /**
     * Assuming the main file is known, set a default auxil path if not already set.
     * Will be set to null if not used.
     */
    fun setDefaultAuxilPath() {
        // -aux-directory pdflatex flag only exists on MiKTeX, so disable auxil otherwise
        if (auxilPath != null || mainFile == null || !latexDistribution.isMiktex()) return
        val moduleRoot = ProjectRootManager.getInstance(project).fileIndex.getContentRootForFile(mainFile!!)
        this.auxilPath = LocalFileSystem.getInstance().findFileByPath(moduleRoot?.path + "/auxil")
    }

    /**
     * Set [auxilPath]
     */
    fun setFileAuxilPath(fileAuxilPath: String) {
        this.auxilPath = findVirtualFileByAbsoluteOrRelativePath(fileAuxilPath, project)
    }

    /**
     * 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 = if (auxilPath == null) {
            false
        }
        else {
            auxilPath != 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()
    }
}

package nl.hannahsten.texifyidea.run.latex

import com.intellij.execution.Location
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 com.intellij.psi.PsiFile
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 = getEntryPointContainer(location) ?: 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.setDefaultAuxilPath()
        runConfiguration.setSuggestedName()
        // Avoid changing the outputPath of the template run config (which is a shallow clone)
        runConfiguration.outputPath = runConfiguration.outputPath.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
    }

    private fun getEntryPointContainer(location: Location<*>?): PsiFile? {
        if (location == null) {
            return null
        }

        val locationElement = location.psiElement
        return locationElement.containingFile
    }

    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.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.Package
import nl.hannahsten.texifyidea.run.compiler.MakeindexProgram
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.PackageUtils
import nl.hannahsten.texifyidea.util.SystemEnvironment
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.includedPackages
import org.jetbrains.annotations.NotNull

/**
 * 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(Magic.Package.index).isNotEmpty()) {
        val makeindexProgram = if (indexPackageOptions.contains("xindy")) MakeindexProgram.XINDY else MakeindexProgram.MAKEINDEX
        indexPrograms.add(makeindexProgram)
    }

    if (Package.GLOSSARIES.name in usedPackages) {
        val glossaryProgram = if (SystemEnvironment.isPerlInstalled) {
            MakeindexProgram.MAKEGLOSSARIES
        }
        else {
            MakeindexProgram.MAKEGLOSSARIESLITE
        }
        indexPrograms.add(glossaryProgram)
    }
    else if (Package.GLOSSARIESEXTRA.name 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 PackageUtils.PACKAGE_COMMANDS }
                .filter { command -> command.requiredParameters.any { it in Magic.Package.index || it in Magic.Package.glossary } }
                .flatMap { it.optionalParameters.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: @NotNull Project): HashMap<String, String> {
    return runReadAction {
        val mainPsiFile = mainFile?.psiFile(project) ?: throw ExecutionException("Main file not found")
        val makeindexOptions = HashMap<String, String>()
        LatexCommandsIndex.getItemsInFileSet(mainPsiFile)
                .filter { it.commandToken.text == "\\makeindex" }
                .forEach {
                    makeindexOptions.putAll(it.optionalParameters)
                }
        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.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
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 {
                    viewer.conversation!!.forwardSearch(pdfPath = runConfig.outputFilePath, sourceFilePath = sourceFilePath, line = line, project = project, focusAllowed = focusAllowed)
                }
                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.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.sumatra.isSumatraAvailable
import nl.hannahsten.texifyidea.util.runCommand

/**
 * List of supported PDF viewers on Linux.
 *
 * @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 PdfViewer(private val viewerCommand: String,
                     val displayName: String,
                     val conversation: ViewerConversation?) {
    EVINCE("evince", "Evince", EvinceConversation),
    OKULAR("okular", "Okular", OkularConversation),
    ZATHURA("zathura", "Zathura", ZathuraConversation),
    SKIM("skim", "Skim", SkimConversation),
    SUMATRA("sumatra", "Sumatra", null), // Dummy options to support Windows
    OTHER("other", "Custom PDF viewer", null);

    /**
     * Check if the viewer is installed and available from the path.
     */
    fun isAvailable(): Boolean {
        // Using a custom PDF viewer should always be an option.
        return if (this == OTHER) {
            true
        }
        else if (SystemInfo.isWindows && this == SUMATRA) {
            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
        }
    }

    companion object {
        fun availableSubset(): List<PdfViewer> = values().filter { it.isAvailable() }
        fun firstAvailable(): PdfViewer = availableSubset().first()
    }
}

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.util.caretOffset
import nl.hannahsten.texifyidea.util.files.openedEditor
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.
     */
    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 mainPsiFile = runConfig.mainFile?.psiFile(environment.project) ?: return
        val editor = mainPsiFile.openedEditor() ?: return

        // Get the line number in the currently open file
        val line = editor.document.getLineNumber(editor.caretOffset()) + 1

        // Get the currently open file to use for forward search.
        val currentPsiFile = editor.document.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 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: HashMap<String, String>,
        private val indexProgram: MakeindexProgram
) : 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 = listOf(indexProgram.executableName, 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

/**
 * 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 WORK_DIR = "work-dir"
    }

    // The following variables are saved in the MakeindexSettingsEditor
    var makeindexProgram: MakeindexProgram = MakeindexProgram.MAKEINDEX
    var mainFile: VirtualFile? = 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)
    }

    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 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(WORK_DIR).apply { text = workingDirectory?.path ?: "" })
    }

    override fun isGeneratedName() = name == suggestedName()

    override fun suggestedName() = mainFile?.nameWithoutExtension + " " + makeindexProgram.name.toLowerCase()

    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 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 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 ?: ""
        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.workingDirectory = LocalFileSystem.getInstance().findFileByPath(workingDirectory.component.text)
    }

    private fun createUIComponents() {
        panel = JPanel().apply {
            layout = VerticalFlowLayout(VerticalFlowLayout.TOP)

            // Program
            val programField = ComboBox<MakeindexProgram>(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)

            // 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.Magic
import nl.hannahsten.texifyidea.util.appendExtension
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 =
                    if (latexRunConfig.makeindexRunConfigs.isEmpty()) {
                        generateIndexConfigs()
                    }
                    else {
                        latexRunConfig.makeindexRunConfigs
                    }

            // 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, Magic.File.bib2glsDependenciesExtensions)
                }

                // Run makeindex
                RunConfigurationBeforeRunProvider.doExecuteTask(environment, runConfigSettings, null)

                copyIndexOutputFiles(baseFileName, Magic.File.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.sumatra

import com.intellij.openapi.util.SystemInfo
import com.pretty_tools.dde.client.DDEClientConversation
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.util.Log
import java.io.IOException

/**
 * 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.
 */
val isSumatraAvailable: Boolean by lazy {
    if (!SystemInfo.isWindows || !isSumatraInstalled()) return@lazy false

    // Try if native bindings are available
    try {
        DDEClientConversation()
    }
    catch (e: UnsatisfiedLinkError) {
        Log.logf("Native library DLLs could not be found.")
        return@lazy false }
    catch (e: NoClassDefFoundError) {
        Log.logf("Native library DLLs could not be found.")
        return@lazy false
    }

    true
}

private fun isSumatraInstalled(): Boolean {
    // Look up SumatraPDF registry key
    val process = Runtime.getRuntime().exec(
            "reg query \"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\SumatraPDF.exe\" /ve"
    )

    val br = process.inputStream.bufferedReader()
    val firstLine = br.readLine() ?: return false
    br.close()

    val sumatraInRegistry = !firstLine.startsWith("ERROR:")

    if (sumatraInRegistry) {
        return true
    }

    // Try if Sumatra is in PATH
    return try {
        Runtime.getRuntime().exec("start SumatraPDF")
        true
    }
    catch (e: IOException) {
        false
    }
}

/**
 * Send commands to SumatraPDF.
 *
 * This is available on Windows only.
 *
 * @author Sten Wessel
 * @since b0.4
 */
object SumatraConversation {

    private const val server = "SUMATRA"
    private const val topic = "control"
    private val conversation: DDEClientConversation?

    init {
        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, sumatraPath: String? = null) {
        try {
            execute("Open(\"$pdfFilePath\", ${newWindow.bit}, ${focus.bit}, ${forceRefresh.bit})")
        }
        catch (e: TeXception) {
            // In case the user provided a custom path to SumatraPDF, add it to the path before executing
            val processBuilder = ProcessBuilder("cmd.exe", "/C", "start", "SumatraPDF", "-reuse-instance", pdfFilePath)
            if (sumatraPath != null) {
                processBuilder.environment()["Path"] = sumatraPath
            }
            processBuilder.start()
        }
    }

    /**
     * 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) {
        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.application.invokeLater
import com.intellij.openapi.util.Key
import nl.hannahsten.texifyidea.TeXception
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
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.openedEditor
import nl.hannahsten.texifyidea.util.files.psiFile
import nl.hannahsten.texifyidea.util.name
import nl.hannahsten.texifyidea.util.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 && (runConfig.sumatraPath != null || isSumatraAvailable)) {
            try {
                SumatraConversation.openFile(runConfig.outputFilePath, sumatraPath = runConfig.sumatraPath)
            }
            catch (ignored: TeXception) {
            }
        }

        // Forward search.
        invokeLater {
            val psiFile = runConfig.mainFile?.psiFile(executionEnvironment.project) ?: return@invokeLater
            val document = psiFile.document() ?: return@invokeLater

            val editor = psiFile.openedEditor() ?: 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.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.BibtexLanguage

class BibtexCodeStyleSettingsProvider : CodeStyleSettingsProvider() {
    override fun createCustomSettings(settings: CodeStyleSettings?) = settings?.let { BibtexCodeStyleSettings(it) }

    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.BibtexLanguage
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.removeHtmlTags

class BibtexLanguageCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider() {
    companion object {
        private val demoText = Magic.General.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.LatexLanguage

/**
 * Defines all the custom code style settings.
 *
 * @author Abby Berkers
 */
@Suppress("PropertyName")
class LatexCodeStyleSettings(container: CodeStyleSettings) : CustomCodeStyleSettings(LatexLanguage.INSTANCE.id, container) {
    /**
     * The number of blank lines to use before a sectioning command.
     */
    @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

    companion object {
        val blankLinesOptions = mapOf(
                LatexCodeStyleSettings::BLANK_LINES_BEFORE_PART to "\\part",
                LatexCodeStyleSettings::BLANK_LINES_BEFORE_CHAPTER to "\\chapter",
                LatexCodeStyleSettings::BLANK_LINES_BEFORE_SECTION to "\\section",
                LatexCodeStyleSettings::BLANK_LINES_BEFORE_SUBSECTION to "\\subsection",
                LatexCodeStyleSettings::BLANK_LINES_BEFORE_SUBSUBSECTION to "\\subsubsection",
                LatexCodeStyleSettings::BLANK_LINES_BEFORE_PARAGRAPH to "\\paragraph",
                LatexCodeStyleSettings::BLANK_LINES_BEFORE_SUBPARAGRAPH to "\\subparagraph"
        )
    }
}

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.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?) = settings?.let { LatexCodeStyleSettings(it) }

    override fun getConfigurableDisplayName() = LatexLanguage.INSTANCE.displayName

    override fun createConfigurable(settings: CodeStyleSettings, originalSettings: CodeStyleSettings): CodeStyleConfigurable {
        return object : CodeStyleAbstractConfigurable(settings, originalSettings, configurableDisplayName) {

            override fun createPanel(settings: CodeStyleSettings?): CodeStyleAbstractPanel {
                val language = LatexLanguage.INSTANCE

                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.INSTANCE && !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.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.INSTANCE
}

class LatexCodeStyleGenerationConfigurable(private val mySettings: CodeStyleSettings) : CodeStyleConfigurable {
    private val myCommenterForm: CommenterForm = CommenterForm(LatexLanguage.INSTANCE)

    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.SmartIndentOptionsEditor
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.LatexLanguage
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.removeHtmlTags

/**
 * Provides the code style settings
 * @author Sten Wessel
 */
class LatexLanguageCodeStyleSettingsProvider : LanguageCodeStyleSettingsProvider() {

    companion object {
        private val demoText = Magic.General.latexDemoText.removeHtmlTags()
    }

    override fun getLanguage() = LatexLanguage.INSTANCE

    override fun getCodeSample(settingsType: SettingsType) = demoText

    override fun getIndentOptionsEditor() = SmartIndentOptionsEditor()

    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,
                    CodeStyleSettingsCustomizable.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.structure.bibtex

import com.intellij.psi.PsiElement
import com.intellij.ui.breadcrumbs.BreadcrumbsProvider
import nl.hannahsten.texifyidea.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.firstChildOfType
import nl.hannahsten.texifyidea.util.substringEnd
import nl.hannahsten.texifyidea.util.tokenType

/**
 * @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?.toLowerCase()) {
                "@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

/**
 * @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.toLowerCase()
        else -> element.text.toLowerCase()
    }

    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.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.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.util.PlatformIcons
import nl.hannahsten.texifyidea.TexifyIcons
import nl.hannahsten.texifyidea.psi.BibtexEntry
import nl.hannahsten.texifyidea.util.*

/**
 * @author Hannah Schellekens
 */
open class BibtexStructureViewEntryElement(val entry: BibtexEntry) : StructureViewTreeElement, SortableTreeElement {

    private val entryPresentation: ItemPresentation = object : ItemPresentation {

        override fun getLocationString() = when (entry.tokenName()?.toLowerCase()) {
            "string" -> entry.tags().first().content?.text.orEmpty()
            "preamble" -> ""
            else -> entry.tokenName()
        }

        override fun getPresentableText() = when (entry.tokenName()?.toLowerCase()) {
            "preamble" -> "preamble"
            "string" -> entry.tags().firstOrNull()?.keyName()
            else -> entry.identifier()
        } ?: ""

        override fun getIcon(b: Boolean) = when (entry.tokenName()?.toLowerCase()) {
            "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.identifier()

    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.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.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: ItemPresentation = object : ItemPresentation {

        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.LatexOtherCommandPresentation
import nl.hannahsten.texifyidea.structure.latex.LatexStructureViewCommandElement
import nl.hannahsten.texifyidea.util.Magic

/**
 * @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 Magic.Command.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

/**
 * @author Hannah Schellekens
 */
object EntryFilter : Filter {

    override fun isVisible(treeElement: TreeElement?): Boolean {
        if (treeElement !is BibtexStructureViewEntryElement) {
            return true
        }

        val name = treeElement.entry.tokenName()?.toLowerCase()
        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.Magic
import javax.swing.Icon

/**
 * @author Hannah Schellekens
 */
class LabelFilter : Filter {
    override fun isVisible(treeElement: TreeElement): Boolean {
        if (treeElement !is LatexStructureViewCommandElement) {
            return true
        }
        return !Magic.Command.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

/**
 * @author Hannah Schellekens
 */
object PreambleFilter : Filter {

    override fun isVisible(treeElement: TreeElement?): Boolean {
        if (treeElement !is BibtexStructureViewEntryElement) {
            return true
        }

        return treeElement.entry.tokenName()?.toLowerCase() != "preamble"
    }

    override fun isReverted() = true

    override fun getName() = "bibtex.texify.filter.preamble"

    override fun getPresentation() = object : ActionPresentation {

        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

/**
 * @author Hannah Schellekens
 */
class SectionFilter : Filter {

    override fun isVisible(treeElement: TreeElement): Boolean {
        return if (treeElement !is LatexStructureViewCommandElement) {
            true
        }
        else !Magic.Command.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

/**
 * @author Hannah Schellekens
 */
object StringFilter : Filter {

    override fun isVisible(treeElement: TreeElement?): Boolean {
        if (treeElement !is BibtexStructureViewEntryElement) {
            return true
        }

        return treeElement.entry.tokenName()?.toLowerCase() != "string"
    }

    override fun isReverted() = true

    override fun getName() = "bibtex.texify.filter.string"

    override fun getPresentation() = object : ActionPresentation {

        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.
        val required = labelCommand.requiredParameters
        if (required.isEmpty()) {
            throw IllegalArgumentException("\\bibitem has no label name")
        }
        this.bibitemName = required[0]

        // 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.LatexLanguage
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.util.name

/**
 * @author Hannah Schellekens
 */
open class LatexBreadcrumbsInfo : BreadcrumbsProvider {

    override fun getLanguages() = arrayOf(LatexLanguage.INSTANCE)

    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.psi.LatexCommands
import nl.hannahsten.texifyidea.structure.EditableHintPresentation

/**
 * @author Hannah Schellekens
 */
class LatexChapterPresentation(chapterCommand: LatexCommands) : EditableHintPresentation {

    private val chapterName: String
    private var hint = ""

    init {
        if (chapterCommand.commandToken.text != "\\chapter") {
            throw IllegalArgumentException("command is no \\chapter-command")
        }

        this.chapterName = chapterCommand.requiredParameters[0]
    }

    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

/**
 * @author Hannah Schellekens
 */
open class LatexFilePresentation(private val file: PsiFile) : ItemPresentation {

    private val presentableText = Magic.Pattern.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.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.CommandManager
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.requiredParameter

/**
 * @author Hannah Schellekens
 */
class LatexLabelPresentation(labelCommand: LatexCommands) : ItemPresentation {

    private val locationString: String
    private val presentableText: String

    init {
        val labelingCommands = Magic.Command.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.psi.LatexCommands
import nl.hannahsten.texifyidea.util.nextCommand

/**
 * @author Hannah Schellekens
 */
class LatexNewCommandPresentation(newCommand: LatexCommands) : ItemPresentation {

    private val newCommandName: String
    private val locationString: String

    init {
        // Fetch parameter amount.
        val optional = newCommand.optionalParameters.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.requiredParameters
        val command = if (required.size > 0) {
            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 value.
        locationString = if (required.size > 1) {
            when (newCommand.commandToken.text) {
                "\\newcommand" -> required[1]
                "\\NewDocumentCommand" -> required[2]
                else -> ""
            }
        }
        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.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.requiredParameters
        newCommandName = if (required.size > 0) {
            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.requiredParameters.isEmpty()) {
            this.paragraphName = ""
        }
        else {
            this.paragraphName = paragraphCommand.requiredParameters[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.requiredParameters[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.psi.LatexCommands
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.getIncludeCommands

/**
 * @author Hannah Schellekens
 */
object LatexPresentationFactory {

    @JvmStatic
    fun getPresentation(commands: LatexCommands): ItemPresentation {
        val labelingCommands = Magic.Command.getLabelDefinitionCommands(commands.project)
        if (labelingCommands.contains(commands.name)) {
            return LatexLabelPresentation(commands)
        }
        return when (commands.name) {
            "\\part" -> LatexPartPresentation(commands)
            "\\chapter" -> LatexChapterPresentation(commands)
            "\\section" -> LatexSectionPresentation(commands)
            "\\subsection" -> LatexSubSectionPresentation(commands)
            "\\subsubsection" -> LatexSubSubSectionPresentation(commands)
            "\\paragraph" -> LatexParagraphPresentation(commands)
            "\\subparagraph" -> LatexSubParagraphPresentation(commands)
            "\\newcommand", "\\DeclareMathOperator", "\\NewDocumentCommand" -> LatexNewCommandPresentation(commands)
            "\\DeclarePairedDelimiter", "\\DeclarePairedDelimiterX", "\\DeclarePairedDelimiterXPP" -> LatexPairedDelimiterPresentation(commands)
            "\\label" -> LatexLabelPresentation(commands)
            "\\bibitem" -> 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.requiredParameters[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.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.Magic
import nl.hannahsten.texifyidea.util.allCommands
import nl.hannahsten.texifyidea.util.getIncludeCommands
import nl.hannahsten.texifyidea.util.getIncludedFiles
import java.util.ArrayDeque
import java.util.Arrays
import java.util.Deque

/**
 * @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?.toLowerCase() ?: if (element is PsiNameIdentifierOwner) {
            element.name!!.toLowerCase()
        }
        else {
            element.text.toLowerCase()
        }
    }

    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.requiredParameters.isNotEmpty() }
                .map { cmd -> cmd.requiredParameters[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 (!Magic.Command.sectionMarkers.contains(token)) {
                continue
            }

            if (currentCmd.requiredParameters.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)

            // Same level.
            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.
        Magic.Command.commandDefinitions.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 = Magic.Command.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 = order(current(sections) ?: continue)

            if (index == indexInsert) {
                registerSameLevel(sections, child, currentCmd, treeElements, numbering)
                break
            }

            if (indexInsert > index) {
                registerDeeper(sections, child, 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.removeFirst()
        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.requiredParameters

        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 { Magic.Command.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) = Magic.Command.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.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.*

/**
 * @author Hannah Schellekens
 */
class LatexStructureViewModel(
        psiFile: PsiFile,
        editor: Editor?
) : StructureViewModelBase(psiFile, editor, LatexStructureViewElement(psiFile)), ElementInfoProvider {

    companion object {

        private val sorterArray = arrayOf(Sorter.ALPHA_SORTER)
        private val filterArray = arrayOf(
                IncludesFilter(),
                SectionFilter(),
                CommandDefinitionFilter(),
                LabelFilter(),
                BibitemFilter()
        )
    }

    override fun getSorters() = sorterArray

    override fun getFilters() = filterArray

    override fun isAlwaysShowsPlus(structureViewTreeElement: StructureViewTreeElement) = false

    override fun isAlwaysLeaf(structureViewTreeElement: StructureViewTreeElement) = false
}

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.requiredParameters[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.requiredParameters[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.requiredParameters[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 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.toLowerCase()

        companion object {

            fun getClassByName(name: String): DocumentClass {
                return if (BOOK.name == name) {
                    BOOK
                }
                else ARTICLE
            }
        }
    }
}

package nl.hannahsten.texifyidea.ui.tablecreationdialog

/**
 * @author Abby Berkers
 */
enum class ColumnType(val displayName: String) {
    TEXT_COLUMN("Text"),
    MATH_COLUMN("Math"),
    NUMBERS_COLUMN("Numbers")
}

package nl.hannahsten.texifyidea.ui.tablecreationdialog

import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.KeyboardShortcut
import com.intellij.openapi.actionSystem.ShortcutSet
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.AnActionButton
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.scale.JBUIScale.scale
import com.intellij.ui.table.JBTable
import com.intellij.util.IconUtil
import nl.hannahsten.texifyidea.action.tablewizard.TableInformation
import java.awt.*
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import javax.swing.*
import javax.swing.border.EmptyBorder

/**
 * Wrapper that contains the table creation dialog. It validates the form when clicking the OK button.
 *
 * @param columnTypes The types of the columns of the table, see [ColumnType], always start with an empty table.
 * @param tableModel The model of the table, always start with an empty table.
 * @param tableInformation Information about the table that is needed to convert it to latex.
 *
 * UI components that have to be validated when clicking the OK button, i.e., checking if the user entered something.
 * @param table The JTable component that shows the table.
 * @param caption The text field that contains the caption for the table.
 * @param reference The text field that contains the label for the table. It has a default value "tab:" to encourage usage
 * of label conventions.
 *
 * @author Abby Berkers
 */
class TableCreationDialogWrapper(private val columnTypes: MutableList<ColumnType> = emptyList<ColumnType>().toMutableList(),
                                 private val tableModel: TableCreationTableModel = TableCreationTableModel(),
                                 var tableInformation: TableInformation = TableInformation(tableModel, columnTypes, "", ""),
        // Components that have to be validated when clicking the OK button.
                                 private val table: JTable = JBTable(tableModel),
                                 private val caption: JBTextField = JBTextField(),
                                 private val reference: JBTextField = JBTextField("tab:")) :
    DialogWrapper(true) {

    init {
        // Initialise the dialog, otherwise it shows as a line (i.e., infinitely small) and without any of the elements.
        init()
    }

    /**
     * Add a table column.
     *
     * @param title of the column.
     * @param columnType is the column type of the column.
     */
    @Suppress("KDocUnresolvedReference")
    private val addColumnFun = fun(title: String, columnType: ColumnType, _: Int) {
        // 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.
     */
    @Suppress("KDocUnresolvedReference", "KDocUnresolvedReference")
    private val editColumnFun = fun(title: String, columnType: ColumnType, columnIndex: Int) {
        tableModel.setHeaderName(title, columnIndex)
        // Edit the column type of the edited column.
        columnTypes[columnIndex] = columnType
        tableModel.fireTableStructureChanged()
    }

    private fun getEditColumnActionButton(): AnActionButton = object : AnActionButton("Edit column header", addText(IconUtil.getEditIcon(), "C")) {

        override fun isEnabled() = table.columnCount > 0

        override fun actionPerformed(e: AnActionEvent) {
            if (table.selectedColumn >= 0) {
                TableCreationEditColumnDialog(
                        editColumnFun,
                        table.selectedColumn,
                        table.getColumnName(table.selectedColumn),
                        columnTypes[table.selectedColumn])
            }
        }
    }

    private fun getAddRowActionButton() = object : AnActionButton("Add Row", addText(IconUtil.getAddIcon(), "R")) {

        override fun isEnabled() = table.columnCount > 0

        override fun actionPerformed(e: AnActionEvent) {
            tableModel.addEmptyRow()
        }
    }

    private fun getRemoveRowActionButton() = object : AnActionButton("Remove Row", addText(IconUtil.getRemoveIcon(), "R")) {

        override fun isEnabled() = table.selectedRow > -1

        override fun actionPerformed(e: AnActionEvent) {
            tableModel.removeRow(table.selectedRow)
        }
    }

    private fun getRemoveColumnActionButton() = object : AnActionButton("Remove Column", addText(IconUtil.getRemoveIcon(), "C")) {

        override fun isEnabled() = table.selectedColumn > -1

        override fun actionPerformed(e: AnActionEvent) {
            tableModel.removeColumn(table.selectedColumn)
        }
    }

    override fun createCenterPanel(): JPanel {

        // Decorator that contains the add/remove/edit buttons.
        val decorator = ToolbarDecorator.createDecorator(table)
                .setAddAction {
                    TableCreationEditColumnDialog(addColumnFun, tableModel.columnCount)
                }
                .setAddActionName("Add Column")
                .setAddIcon(addText(IconUtil.getAddIcon(), "C"))
                .addExtraAction(getRemoveColumnActionButton())
                .addExtraAction(getEditColumnActionButton())
                .addExtraAction(getAddRowActionButton())
                .addExtraAction(getRemoveRowActionButton().apply { shortcut = ShortcutSet { arrayOf(KeyboardShortcut(KeyStroke.getKeyStroke("DELETE"), null)) } })
                .createPanel()

        table.addTabCreatesNewRowAction()
        table.addEnterCreatesNewRowAction()

        val captionLabel = JBLabel("Caption:")
        captionLabel.labelFor = caption

        val referenceLabel = JBLabel("Label:")
        referenceLabel.labelFor = reference

        // Add all elements to the panel view.
        val panel = JPanel()
        panel.apply {
            // Add some air around the elements.
            border = EmptyBorder(8, 8, 8, 8)
            layout = BoxLayout(this, BoxLayout.Y_AXIS)

            // Create a panel for the table and its decorator.
            val tablePanel = JPanel()
            tablePanel.apply {
                layout = BorderLayout()
                add(JScrollPane(table), BorderLayout.WEST)
                add(decorator, BorderLayout.EAST)
            }

            // Help text
            val helpText = JBLabel("<html>Press tab to go to the next cell or row, press enter to go to the next row.</html>")
            helpText.foreground = Color.GRAY

            // Put help text below table
            val tablePanelContainer = JPanel(GridBagLayout())
            val constraints = GridBagConstraints()
            constraints.gridx = 0
            constraints.gridy = GridBagConstraints.RELATIVE
            tablePanelContainer.apply {
                add(tablePanel, constraints)
                add(helpText, constraints)
            }
            add(tablePanelContainer)

            // Create a panel for the caption box and its label.
            val captionPanel = JPanel()
            captionPanel.apply {
                layout = BoxLayout(this, BoxLayout.X_AXIS)
                captionLabel.preferredSize = Dimension(80, captionLabel.height)
                add(captionLabel)
                add(caption)
            }

            // Create a panel for the label/reference box and its label.
            val referencePanel = JPanel()
            referencePanel.apply {
                layout = BoxLayout(this, BoxLayout.X_AXIS)
                referenceLabel.preferredSize = Dimension(80, referenceLabel.height)
                add(referenceLabel)
                add(reference)
            }

            // Actually add all the panels to the main panel.
            // Add some air between components.
            add(Box.createRigidArea(Dimension(0, 8)))
            add(captionPanel)
            add(Box.createRigidArea(Dimension(0, 8)))
            add(referencePanel)
        }

        return panel
    }

    /**
     * See [IconUtil.addText].
     */
    fun addText(base: Icon, text: String, scale: Float = 7f): Icon? {
        val icon = LayeredIcon(2)
        icon.setIcon(base, 0, SwingConstants.NORTH_WEST)
        icon.setIcon(IconUtil.textToIcon(text, JLabel(), scale(scale)), 1, SwingConstants.SOUTH_EAST)
        return icon
    }

    /**
     * When clicking OK, the wrapper will validate the form. This means that the table should at least have a header,
     * there is some text in the caption text field, and the label text field contains more than just "tab:" (or no
     * "tab:" at all, but then it should not be empty).
     */
    override fun doValidate(): ValidationInfo? {
        return if (tableModel.getColumnNames().size == 0) ValidationInfo("Table cannot be empty.", table)
        else if (caption.text.isEmpty()) ValidationInfo("Caption cannot be empty.", caption)
        else if (reference.text.isEmpty() || reference.text == "tab:") ValidationInfo("Label cannot be empty", reference)
        else {
            // 'Save' the current values in the form.
            tableInformation = TableInformation(tableModel, columnTypes, caption.text, reference.text)
            return null
        }
    }

    /**
     * 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)
    }
}

package nl.hannahsten.texifyidea.ui.tablecreationdialog

import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import javax.swing.JComboBox
import javax.swing.JPanel

/**
 * Dialog to add a new column to the table.
 *
 * @param onOkFunction The function to execute when clicking the OK button.
 * @param editingColumn The index of the column being edited.
 * @param columnName The name of the column that is being edited. Default is the empty string, the title of a column that does
 *          not yet exist.
 * @param columnType The [ColumnType] of the column that is being edited. Default is a text column.
 *
 * @author Abby Berkers
 */
class TableCreationEditColumnDialog(
        private val onOkFunction: (String, ColumnType, Int) -> Unit,
        private val editingColumn: Int,
        private val columnName: String = "",
        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 columnNameField = JBTextField(columnName)
            val columnNameLabel = JBLabel("Column name")
            columnNameLabel.labelFor = columnNameField

            // A combobox to select the column type.
            val columnTypeComboBox = JComboBox(ColumnType.values().map { it.displayName }.toTypedArray())
            // Select the old type of the editing column.
            columnTypeComboBox.selectedIndex = ColumnType.values().indexOf(columnType)
            val columnTypeLabel = JBLabel("Column type")
            columnTypeLabel.labelFor = columnTypeComboBox

            // Add UI elements.
            val panel = JPanel()
            panel.apply {
                add(columnNameLabel)
                add(columnNameField)
                add(columnTypeLabel)
                add(columnTypeComboBox)
            }
            setCenterPanel(panel)
            setPreferredFocusComponent(columnNameField)

            addOkAction()
            setOkOperation {
                dialogWrapper.close(0)
            }

            if (columnName == "") {
                title("Add column")
            }
            else {
                title("Edit column")
            }

            if (show() == DialogWrapper.OK_EXIT_CODE) {
                onOkFunction(columnNameField.text, ColumnType.values()[columnTypeComboBox.selectedIndex], editingColumn)
            }
        }
    }
}

package nl.hannahsten.texifyidea.ui.tablecreationdialog

import java.util.*
import javax.swing.table.DefaultTableModel

/**
 * @author Abby Berkers
 */
class TableCreationTableModel : DefaultTableModel() {
    /**
     * 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 = (0 until columnCount).map { "" }.toTypedArray()
        addRow(emptyRow)
    }

    /**
     * Get the names of the columns.
     */
    fun getColumnNames(): Vector<Any> = columnIdentifiers
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
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.psi.LatexNormalText
import nl.hannahsten.texifyidea.util.childrenOfType
import nl.hannahsten.texifyidea.util.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.
 *
 * @param baseFile
 *         The file to find the reference set of.
 * @return All the files that are cross referenced between each other.
 */
// Internal because only ReferencedFileSetCache should call this
internal fun findReferencedFileSetWithoutCache(baseFile: PsiFile): Set<PsiFile> {
    // Setup.
    val project = baseFile.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 = root.referencedFiles(root.virtualFile) + root

        if (referenced.contains(baseFile)) {
            return referenced + baseFile
        }

        sets[root] = referenced
    }

    // Look for matching root.
    for (referenced in sets.values) {
        if (referenced.contains(baseFile)) {
            return referenced + baseFile
        }
    }

    return setOf(baseFile)
}

/**
 * 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]
 */
@Suppress("unused")
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)
}

/**
 * When using \includegraphics from graphicx package, a path prefex can be set with \graphicspath.
 * @return Graphicspaths defined in the fileset.
 */
fun getGraphicsPaths(project: Project): List<String> {

    val graphicsPaths = mutableListOf<String>()
    val graphicsPathCommands = LatexCommandsIndex.getItemsByName("\\graphicspath", project, GlobalSearchScope.projectScope(project))

    // Is a graphicspath defined?
    if (graphicsPathCommands.isNotEmpty()) {
        // Only last defined one counts
        val args = graphicsPathCommands.last().parameterList.filter { it.requiredParam != null }
        // These arguments have to be in a group in a parameter, and a group contains LatexNormalText instead of LatexParameterText
        val subArgs = args.first().childrenOfType(LatexNormalText::class)
        subArgs.forEach { graphicsPaths.add(it.text) }
    }

    return graphicsPaths
}

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.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 nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.util.Magic
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.regex.Pattern

/**
 * @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 Magic.File.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 {
    var count = 0
    var currentFileName = fileName
    while (File(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 File(currentFileName).apply {
        createNewFile()
        LocalFileSystem.getInstance().refresh(true)
        writeText(contents, StandardCharsets.UTF_8)
    }
}

/**
 * Get a(n external) file by its absolute path.
 */
fun getExternalFile(path: String): VirtualFile? =
        LocalFileSystem.getInstance().findFileByPath(path)

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.psi.LatexCommands
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.Magic
import nl.hannahsten.texifyidea.util.Magic.Command.relativeImportCommands
import nl.hannahsten.texifyidea.util.appendExtension
import nl.hannahsten.texifyidea.util.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.
 */
fun searchFileByImportPaths(command: LatexCommands): PsiFile? {
    // 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
    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> {
    // Check if import commands are used (do this now, to only search for import paths when needed)
    val allRelativeImportCommands = LatexIncludesIndex.getCommandsByNames(Magic.Command.relativeImportCommands, command.project, GlobalSearchScope.projectScope(command.project))
    val allAbsoluteImportCommands = LatexIncludesIndex.getCommandsByNames(Magic.Command.absoluteImportCommands, command.project, GlobalSearchScope.projectScope(command.project))
    if (allAbsoluteImportCommands.isEmpty() && allRelativeImportCommands.isEmpty()) {
        return emptyList()
    }

    checkForAbsolutePath(command)?.let { return listOf(it) }

    val relativeSearchPaths = mutableListOf<String>()
    if (command.name in Magic.Command.relativeImportCommands) {
        relativeSearchPaths.add(command.requiredParameters.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 Magic.Command.absoluteImportCommands) {
        val absolutePath = command.requiredParameters.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.requiredParameters.any { it.contains(command.containingFile.name.removeFileExtension()) } }

    if (includingCommands.isEmpty() && command.name in relativeImportCommands) {
        command.containingFile.containingDirectory?.virtualFile?.findFileByRelativePath(command.requiredParameter(0) ?: "")?.let { return listOf(it) }
    }

    // Avoid endless loop (in case of a file inclusion loop)
    val maxDepth = allIncludeCommands.size
    var counter = 0

    // Final paths
    val absoluteSearchDirs = mutableListOf<VirtualFile>()

    // 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 Magic.Command.relativeImportCommands) {
                    newSearchPaths.add(includingCommand.requiredParameters.firstOrNull() + oldPath)
                }
            }

            // Find files/commands to search next
            val file = includingCommand.containingFile
            if (file !in handledFiles) {
                val commandsIncludingThisFile = allIncludeCommands.filter { includeCommand -> includeCommand.requiredParameters.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 nl.hannahsten.texifyidea.run.latex.LatexDistribution
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader

/**
 * Cache locations of LaTeX packages in memory, because especially on Windows they can be expensive to retrieve
 * (requires a run of kpsewhich).
 */
object LatexPackageLocationCache {

    private val cache = mutableMapOf<String, String?>()

    /**
     * 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) = cache.getOrPut(name) {
        runKpsewhich(
            name
        )
    }

    private fun runKpsewhich(arg: String): String? = try {
        val command = if (LatexDistribution.isMiktexAvailable) {
            // Don't install the package if not present
            "miktex-kpsewhich --miktex-disable-installer $arg"
        }
        else {
            "kpsewhich $arg"
        }
        BufferedReader(
            InputStreamReader(Runtime.getRuntime().exec(command).inputStream)
        ).readLine() // Returns null if no line read.
    }
    catch (e: IOException) {
        null
    }
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileEditorManager
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.Package
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.psi.LatexEnvironment
import nl.hannahsten.texifyidea.reference.InputFileReference
import nl.hannahsten.texifyidea.util.*

/**
 * 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(): PsiFile? {
    val command = commandsInFile().asSequence()
            .filter { it.name == "\\documentclass" }
            .firstOrNull() ?: return null
    val argument = command.requiredParameter(0) ?: return null
    return findFile("$argument.cls")
}

/**
 * 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) = PackageUtils.getIncludedPackages(this).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`: Package) = 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) }
                .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? {
    val directory = containingDirectory?.virtualFile

    val file = directory?.findFile(path, extensions
            ?: Magic.File.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 = Magic.File.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
                ?: Magic.File.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)

/**
 * @see [LatexCommandsIndex.getItems]
 */
fun PsiFile.commandsInFile(): Collection<LatexCommands> = LatexCommandsIndex.getItems(this)

/**
 * @see [LatexEnvironmentsIndex.getItems]
 */
fun PsiFile.environmentsInFile(): Collection<LatexEnvironment> = LatexEnvironmentsIndex.getItems(this)

/**
 * Get the editor of the file if it is currently opened.
 */
fun PsiFile.openedEditor() = FileEditorManager.getInstance(project).selectedTextEditor

/**
 * 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)
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
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.
     */
    private val fileSetCache = ConcurrentHashMap<VirtualFile, Set<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.
     */
    private var numberOfIncludes = 0

    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 if (file.virtualFile != null) {
            // Use the keys of the whole project, because suppose a new include includes the current file, it could be anywhere in the project
            val numberOfIncludesChanged = if (LatexIncludesIndex.getItems(file.project).size != numberOfIncludes) {
                numberOfIncludes = LatexIncludesIndex.getItems(file.project).size
                dropAllCaches()
                true
            }
            else {
                false
            }

            // 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 {
                mutex.withLock {
                    if (!fileSetCache.containsKey(file.virtualFile) || numberOfIncludesChanged) {
                        runReadAction {
                            fileSetCache[file.virtualFile] = findReferencedFileSetWithoutCache(file)
                        }
                    }
                }
            }
            fileSetCache[file.virtualFile] ?: setOf(file)
        }
        else {
            setOf(file)
        }
    }

    /**
     * Clears the cache for base file `file`.
     */
    fun dropCaches(file: VirtualFile) {
        fileSetCache.remove(file)
    }

    fun dropAllCaches() {
        fileSetCache.keys.forEach { fileSetCache.remove(it) }
    }
}

package nl.hannahsten.texifyidea.util.files

import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile

/**
 * @author Hannah Schellekens
 */
interface ReferencedFileSetService {

    companion object {

        @JvmStatic
        fun getInstance(): ReferencedFileSetService {
            return ServiceManager.getService(ReferencedFileSetService::class.java)
        }
    }

    /**
     * [findReferencedFileSetWithoutCache], but then with cached values.
     */
    fun referencedFileSetOf(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.execution.RunManager
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import nl.hannahsten.texifyidea.algorithm.IsChildDFS
import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.index.LatexIncludesIndex
import nl.hannahsten.texifyidea.lang.magic.DefaultMagicKeys
import nl.hannahsten.texifyidea.lang.magic.magicComment
import nl.hannahsten.texifyidea.psi.LatexCommands
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import java.util.HashMap

/**
 * Scans all file inclusions and finds the file that is at the base of all inclusions.
 *
 * When no file is included, `this` file will be returned.
 */
fun PsiFile.findRootFile(): PsiFile {
    val magicComment = magicComment()
    if (magicComment.contains(DefaultMagicKeys.ROOT)) {
        val path = magicComment.value(DefaultMagicKeys.ROOT) ?: ""
        this.findFile(path)?.let { return it }
    }

    if (this.isRoot()) {
        return this
    }

    // We need to scan all file inclusions in the project, because any file could include the current file
    val inclusions = project.allFileInclusions()

    inclusions.forEach { (file, _) ->
        // Function to avoid unnecessary evaluation
        fun usesSubFiles() = file.commandsInFile()
                .find { it.name == "\\documentclass" }
                ?.requiredParameters
                ?.contains("subfiles") == true

        // For each root, IsChildDFS until found.
        if (!file.isRoot() || usesSubFiles()) {
            return@forEach
        }

        // If the root file contains this, we have found the root file
        if (file.contains(this, inclusions)) {
            return file
        }
    }

    return this
}

/**
 * Checks if the given file is included by `this` file.
 *
 * @param childMaybe
 *              The file to check for if it is a child of this file.
 * @param mapping
 *              Map that maps each psi file to all the files that get included by said file.
 * @return `true` when `childMaybe` is a child of `this` file, `false` otherwise.
 */
private fun PsiFile.contains(childMaybe: PsiFile, mapping: Map<PsiFile, Set<PsiFile>>): Boolean {
    return IsChildDFS(
            this,
            { mapping[it] ?: emptySet() },
            { childMaybe == it }
    ).execute()
}

/**
 * Looks up all the files that include each other.
 * This may be an expensive function when a lot of include commands and files are present.
 *
 * @return A map that maps each file to a set of all files that get included by said file. E.g.
 * when `A`↦{`B`,`C`}. Then the files `B` and `C` get included by `A`.
 */
private fun Project.allFileInclusions(): Map<PsiFile, Set<PsiFile>> {
    val allIncludeCommands = LatexIncludesIndex.getItems(this)

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

        val includedNames = command.getAllRequiredArguments() ?: continue

        var foundFile = false

        for (includedName in includedNames) {
            val referenced = declaredIn.findFile(includedName)
                    ?: continue

            foundFile = true

            // When it looks like a file includes itself, we skip it
            if (declaredIn.viewProvider.virtualFile.nameWithoutExtension == includedName) {
                continue
            }

            inclusions.getOrPut(declaredIn) { mutableSetOf() }.add(referenced)
        }

        if (!foundFile) {
            // Check if import package is used
            searchFileByImportPaths(command)?.let {
                inclusions.getOrPut(declaredIn) { mutableSetOf() }.add(it)
            }
        }
    }

    return inclusions
}

/**
 * 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.commandToken.text == "\\documentclass" }

    // Whether the document makes use of the subfiles class, in which case it is not a root file
    fun usesSubFiles() = documentClass()?.requiredParameters?.contains("subfiles") == 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 runManager = RunManagerImpl.getInstanceImpl(project) as RunManager
    val isMainFileInAnyConfiguration = runManager.allConfigurationsList.filterIsInstance<LatexRunConfiguration>().any { it.mainFile == this.virtualFile }

    return isMainFileInAnyConfiguration || documentClass() != null && !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 = requiredParameters
    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.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? {
    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
    }

    return null
}

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.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

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().toLowerCase().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().toLowerCase()
    }
}

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.TeXception
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex.biberNumberOfErrors
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex.biberNumberOfWarnings
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex.bibtexNumberOfErrors
import nl.hannahsten.texifyidea.run.bibtex.logtab.BibtexLogMagicRegex.bibtexNumberOfWarnings
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?,
    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)

        // Check for summary in the middle of the window, otherwise we might
        // be checking the summary before we processed all the messages
        if (window.size > 2) {
            compareNumberOfMessages(window.toList()[window.size - 3] as String)
        }

        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)
        }
    }

    /**
     * Compare the summary of the number of warnings as given by bibtex/biber
     * with our own message list.
     */
    private fun compareNumberOfMessages(newText: String) {
        val warningRegexes = listOf(bibtexNumberOfWarnings, biberNumberOfWarnings)
        val errorRegexes = listOf(bibtexNumberOfErrors, biberNumberOfErrors)
        if ((warningRegexes + errorRegexes).any { it.containsMatchIn(newText) }) {
            checkMessageNumber(warningRegexes, newText, BibtexLogMessageType.WARNING)
            checkMessageNumber(errorRegexes, newText, BibtexLogMessageType.ERROR)
        }
    }

    private fun checkMessageNumber(regexes: List<Regex>, newText: String, type: BibtexLogMessageType) {
        for (regex in regexes) {
            regex.find(newText)?.apply {
                val number = try {
                    groups["number"]?.value?.toInt() ?: 1
                }
                catch (e: IllegalArgumentException) {
                    1
                }
                if (number != messageList.filter { it.type == type }.size) {
                    throw TeXception("Incorrect number of ${type}s parsed. Please report the bibtex/biber log to the issue tracker.")
                }
            }
        }
    }

    fun addBibMessageToTree(logMessage: BibtexLogMessage) {
        treeView.applyFilters(logMessage)
    }

    /**
     * Let all message handlers try to find a message based on the window.
     */
    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.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.ArrayDeque

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()

    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-\d. :])+"?)|\)""")
        }
        else {
            Regex("""\((?<file>"?\.*(([/\\])*[\w-\d. :])+\.(\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] == '(') {
                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()) {
                        throw TeXception("Extra closing parenthesis: could not close a file which was not opened. Please report the log output to the issue tracker. Line content: $line")
                    }
                    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 ...
    const val FILE_LINE_REGEX: String = """(?!\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>[\d\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

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().toLowerCase().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.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
        )

        // Look for errors that need special treatment.
        specialErrorHandlersList.forEach { handler ->
            if (handler.regex.any { it.containsMatchIn(text) }) {
                return handler.findMessage(text, newText, currentFile)
            }
        }

        // Handles all other file line errors. Only check the first line,
        // because other errors might need the two lines, and would be
        // (partly) duplicated in the log if we allow the fallback to inspect
        // the two lines (or just the first).
        if (LatexErrorHandler.regex.any { it.containsMatchIn(text.removeSuffix(newText)) }) {
            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
        // Assumes
        if (TEX_MISC_WARNINGS.any { text.removeSuffix(newText).startsWith(it) }) {
            var messageText = if (LatexOutputListener.isLineEndOfMessage(newText, text)) text.remove(newText) else text

            // Don't include the second line if it is not part of the message
            // (Do this before cleaning up the messageText)
            if (LatexOutputListener.isLineEndOfMessage(newText, text.remove(newText))) {
                messageText = messageText.remove(newText).trim()
            }

            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?,
    val messageList: MutableList<LatexLogMessage>,
    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)
        }
    }

    /**
     * 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)

    var isCollectingMessage = false
    var currentLogMessage: LatexLogMessage? = null

    // Stack with the filenames, where the first is the current file.
    private var fileStack = LatexFileStack()

    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

        // 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(event.text)
    }

    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(), " ")

            // 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.LatexDistribution
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.makeindex.MakeindexRunConfigurationType
import java.awt.event.ItemEvent
import javax.swing.JComponent
import javax.swing.JPanel

/**
 * @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 bibliographyPanel: RunConfigurationPanel<BibtexRunConfigurationType>
    private lateinit var makeindexPanel: RunConfigurationPanel<MakeindexRunConfigurationType>

    /** 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

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

        // 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?.path ?: ""
        }

        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 bibliography
        bibliographyPanel.configurations = runConfiguration.bibRunConfigs.toMutableSet()

        // Reset makeindex
        makeindexPanel.configurations = if (runConfiguration.makeindexRunConfigs.isNotEmpty()) runConfiguration.makeindexRunConfigs.toMutableSet() else mutableSetOf()
    }

    // 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
        if (runConfiguration.compiler?.includesBibtex == true) {
            runConfiguration.bibRunConfigs = setOf()
            bibliographyPanel.isVisible = false
        }
        else {
            bibliographyPanel.isVisible = true

            // Apply bibliography, only if not hidden
            runConfiguration.bibRunConfigs = bibliographyPanel.configurations
        }

        if (runConfiguration.compiler?.includesMakeindex == true) {
            runConfiguration.makeindexRunConfigs = emptySet()
            makeindexPanel.isVisible = false
        }
        else {
            makeindexPanel.isVisible = true

            // Apply makeindex
            runConfiguration.makeindexRunConfigs = makeindexPanel.configurations
        }

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

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

    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
        latexDistribution = LabeledComponent.create(ComboBox(LatexDistributionType.values().filter { it.isInstalled() }.toTypedArray()), "LaTeX Distribution")
        panel.add(latexDistribution)

        panel.add(extensionSeparator)

        // Extension panels
        bibliographyPanel = RunConfigurationPanel(project!!, "Bibliography: ", BibtexRunConfigurationType::class.java)
        panel.add(bibliographyPanel)

        makeindexPanel = RunConfigurationPanel(project!!, "Makeindex: ", MakeindexRunConfigurationType::class.java)
        panel.add(makeindexPanel)
    }

    private fun addOutputPathField(panel: JPanel) {
        // The aux directory is only available on MiKTeX, so only allow disabling on MiKTeX
        if (LatexDistribution.isMiktexAvailable) {

            val auxilPathField = TextFieldWithBrowseButton()
            auxilPathField.addBrowseFolderListener(
                    TextBrowseFolderListener(
                            FileChooserDescriptor(false, true, false, false, false, false)
                                    .withTitle("Choose a directory for auxiliary files")
                                    .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("Choose a directory for output files.")
                                .withRoots(*ProjectRootManager.getInstance(project!!)
                                        .contentRootsFromAllModules)
                )
        )
        outputPath = LabeledComponent.create(outputPathField, "Directory for output files (use directory of main file when using BiBTeX without MiKTeX),\n or 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)
    }

    /**
     * Optional custom path for SumatraPDF.
     */
    private fun addSumatraPathField(panel: JPanel) {
        if (SystemInfo.isWindows) {
            enableSumatraPath = JBCheckBox("Select custom path to SumatraPDF")
            panel.add(enableSumatraPath)

            sumatraPath = TextFieldWithBrowseButton().apply {
                addBrowseFolderListener(
                        TextBrowseFolderListener(
                                FileChooserDescriptor(false, true, false, false, false, false)
                                        .withTitle("Choose the folder which contains SumatraPDF.exe")
                        )
                )

                isEnabled = false

                addPropertyChangeListener("enabled") { e ->
                    if (!(e.newValue as Boolean)) {
                        this.setText(null)
                    }
                }
            }

            enableSumatraPath.addItemListener { e -> sumatraPath.isEnabled = e.stateChange == ItemEvent.SELECTED }

            panel.add(sumatraPath)
        }
    }

    /**
     * Optional custom pdf viewer command text field.
     */
    private fun addPdfViewerCommandField(panel: JPanel) {
        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
        )
    }
}

package nl.hannahsten.texifyidea.run.latex.ui

import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.configurations.ConfigurationType
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.openapi.actionSystem.ActionToolbarPosition
import com.intellij.openapi.project.Project
import com.intellij.ui.HideableTitledPanel
import com.intellij.ui.ToolbarDecorator
import com.intellij.ui.components.JBList
import com.intellij.util.ui.JBUI
import java.awt.BorderLayout
import javax.swing.DefaultListSelectionModel
import javax.swing.JPanel

/**
 * @author Sten Wessel
 */
class RunConfigurationPanel<RunConfigurationType : ConfigurationType>(
        private val project: Project,
        private val title: String,
        private val runConfigurationType: Class<RunConfigurationType>) : JPanel(BorderLayout()) {

    private val contentPanel = JPanel(BorderLayout())
    private val hidePanel: HideableTitledPanel
    private lateinit var list: JBList<RunnerAndConfigurationSettings>

    var configurations: MutableSet<RunnerAndConfigurationSettings> = mutableSetOf()
        set(value) {
            field = value
            configurationChanged()
        }

    init {

        createPanel()
        hidePanel = HideableTitledPanel(title, false).apply {
            setContentComponent(contentPanel)
        }
        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)
                configurationChanged()
            }
        }

        contentPanel.add(toolbar.createPanel(), BorderLayout.CENTER)
    }

    private fun askRunConfigurations(): List<RunnerAndConfigurationSettings> {
        val configurations = RunManagerImpl.getInstanceImpl(project).allSettings.filter { it.type.javaClass == runConfigurationType }

        val dialog = RunConfigurationSelectionDialog(project, configurations)

        dialog.show()

        return dialog.selected
    }

    private fun changeTitle(titleSuffix: String) {
        hidePanel.title = 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) }.max()
                    ?: 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 org.freedesktop.dbus.connections.impl.DBusConnection
import org.freedesktop.dbus.errors.NoReply
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) {
        // Will do nothing if file is already open in Evince
        findProcessOwner(pdfFilePath)
    }

    /**
     * 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)
        }

        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"
            Runtime.getRuntime().exec(arrayOf("bash", "-c", command))
        }
        else {
            // If the user used the forward search menu action
            if (pdfPath == null) {
                Notification("EvinceConversation", "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) {
        // 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) {}
    }
}

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.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import nl.hannahsten.texifyidea.util.runCommand
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 currentCoroutineJob: Job? = null

    /**
     * Starts a listener which listens for inverse search actions from Evince.
     */
    fun start(project: Project) {
        // Check if Evince version supports dbus
        // Technically only version 2.32 is needed, but since 3.0 was released back
        // in 2011 we just check for major version 3, much easier

        try {
            // Assumes version will be given in the format GNOME Document Viewer 3.34.2
            val majorVersion = "evince --version".runCommand()
                    ?.split(" ")
                    ?.lastOrNull()
                    ?.split(".")
                    ?.firstOrNull()
                    ?.toInt()
            if (majorVersion != null && majorVersion < 3) {
                Notification("EvinceInverseSearchListener", "Old Evince version found", "Please update Evince to at least version 3 to use forward/backward search", NotificationType.ERROR).notify(project)
                return
            }
        }
        catch (ignored: NumberFormatException) {}

        // Run in a coroutine so the main thread can continue
        // If the program finishes, the listener will stop as well
        currentCoroutineJob = GlobalScope.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 && currentCoroutineJob?.isActive == true) {
                // 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 (currentCoroutineJob?.isActive == false) 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()
        }
    }

    suspend fun unload() {
        currentCoroutineJob?.cancelAndJoin()
    }
}

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 we are not allowed to change focus, we cannot open the pdf or do forward search because this will always change focus with Okular
        if (!focusAllowed) {
            return
        }

        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 --unique '$pdfFilePath#src:$line $sourceFilePath'"
            Runtime.getRuntime().exec(arrayOf("bash", "-c", command))
        }
        else {
            Notification("OkularConversation", "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("SkimConversation", "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

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} $sourceFilePath" $pdfPathGuess"""
            Runtime.getRuntime().exec(arrayOf("bash", "-c", command))
        }
        else {
            Notification("ZathuraConversation", "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 = selectedRunConfig(project) ?: 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 currently selected run configuration, IF it is a [LatexRunConfiguration].
     * Otherwise returns null.
     */
    private fun selectedRunConfig(project: Project): LatexRunConfiguration? = try {
            (RunManagerImpl.getInstanceImpl(project) as RunManager)
                    .selectedConfiguration
                    ?.configuration
                    as LatexRunConfiguration
    }
    catch (e: ClassCastException) {
        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.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 dropCaches(file: VirtualFile) = cache.dropCaches(file)

    override fun dropAllCaches() = cache.dropAllCaches()
}

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.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.ToggleAction
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

class LatexCompileMessageTreeView(
    val project: Project,
    val latexMessageList: MutableList<LatexLogMessage>,
    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.toLowerCase().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()
        }
    }

    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()
        }
    }

    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().associate { it to 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 =
            ServiceManager.getService(
                project,
                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
) {
    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.mapNotNull { p ->
                    if (p.regex.any { r -> r.containsMatchIn(message) }) p.postProcess(p.process(message)) else null
                }
                    .firstOrNull() ?: 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,
        """^$FILE_LINE_REGEX (?<message>Undefined control sequence.)\s*l.\d+\s*(?<command>\\\w+)$""".toRegex(),
        """^$LATEX_ERROR_REGEX (?<message>Undefined control sequence.)\s*l.\d+\s*(?<command>\\\w+)$""".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} ${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.LatexLogMessage
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageHandler
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

/**
 * 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.LatexLogMessageType
import nl.hannahsten.texifyidea.run.latex.logtab.LatexMessageWithLineHandler
import nl.hannahsten.texifyidea.run.latex.logtab.LatexLogMagicRegex

/**
 * 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()
)