Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Root files #1617

Merged
merged 8 commits into from Oct 17, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .editorconfig
@@ -1,5 +1,6 @@
[*.{kt,kts}]
disabled_rules = final-newline,no-wildcard-imports,parameter-list-wrapping,import-ordering,keyword-spacing
# Enable indent once ktlint supports continuation indent again.
disabled_rules = final-newline,no-wildcard-imports,parameter-list-wrapping,import-ordering,keyword-spacing,indent
insert_final_newline = false

ij_kotlin_else_on_new_line = true
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Expand Up @@ -22,7 +22,7 @@ plugins {
}

group = "nl.hannahsten"
version = "0.7.1-alpha.7"
version = "0.7.1-alpha.8"

repositories {
mavenCentral()
Expand Down
3 changes: 3 additions & 0 deletions resources/META-INF/plugin.xml
Expand Up @@ -644,6 +644,9 @@
<localInspection language="Latex" implementationClass="nl.hannahsten.texifyidea.inspections.latex.LatexEscapeAmpersandInspection"
groupName="LaTeX" displayName="Unescaped &amp; character"
enabledByDefault="true"/>
<localInspection language="Latex" implementationClass="nl.hannahsten.texifyidea.inspections.latex.LatexDocumentclassNotInRootInspection"
groupName="LaTeX" displayName="File that contains a document environment should contain a \documentclass command"
enabledByDefault="true"/>

<!-- Element manipulator -->
<lang.elementManipulator forClass="nl.hannahsten.texifyidea.psi.impl.LatexEnvironmentImpl"
Expand Down
@@ -0,0 +1,11 @@
<html>
<body>
<p>
The root file of a LaTeX document should contain a <tt>\documentclass</tt> command, then the document preamble, and finally the <tt>document</tt> environment.
</p>
<!-- tooltip end -->
<p>
See the <a href="https://github.com/Hannah-Sten/TeXiFy-IDEA/wiki/LaTeX-recommendations#ins:documentclass">wiki</a> for more information.
</p>
</body>
</html>
@@ -0,0 +1,44 @@
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.LatexEnvironment
import nl.hannahsten.texifyidea.util.files.commandsInFile
import nl.hannahsten.texifyidea.util.firstChildOfType
import org.jetbrains.annotations.Nls

class LatexDocumentclassNotInRootInspection : TexifyInspectionBase() {
override val inspectionGroup: InsightGroup
get() = InsightGroup.LATEX

@Nls
override fun getDisplayName(): String {
return "Documentclass command should be in the same file as the document environment"
}

override val inspectionId: String
get() = "DocumentclassNotInRoot"

override fun inspectFile(file: PsiFile, manager: InspectionManager, isOntheFly: Boolean): List<ProblemDescriptor> {
val documentClass = file.commandsInFile().find { it.name == "\\documentclass" } ?: return emptyList()

val hasDocumentEnvironment = file.firstChildOfType(LatexEnvironment::class)?.environmentName == "document"

if (!hasDocumentEnvironment) {
return listOf(
manager.createProblemDescriptor(
documentClass,
displayName,
true,
ProblemHighlightType.WARNING,
isOntheFly
)
)
}
return emptyList()
}
}
14 changes: 7 additions & 7 deletions src/nl/hannahsten/texifyidea/util/files/FileSet.kt
Expand Up @@ -25,9 +25,9 @@ import nl.hannahsten.texifyidea.util.isDefinition
* @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> {
internal fun PsiFile.findReferencedFileSetWithoutCache(): Set<PsiFile> {
// Setup.
val project = baseFile.project
val project = this.project
val includes = LatexIncludesIndex.getItems(project)

// Find all root files.
Expand All @@ -42,21 +42,21 @@ internal fun findReferencedFileSetWithoutCache(baseFile: PsiFile): Set<PsiFile>
for (root in roots) {
val referenced = root.referencedFiles(root.virtualFile) + root

if (referenced.contains(baseFile)) {
return referenced + baseFile
if (referenced.contains(this)) {
return referenced + this
}

sets[root] = referenced
}

// Look for matching root.
for (referenced in sets.values) {
if (referenced.contains(baseFile)) {
return referenced + baseFile
if (referenced.contains(this)) {
return referenced + this
}
}

return setOf(baseFile)
return setOf(this)
}

/**
Expand Down
44 changes: 30 additions & 14 deletions src/nl/hannahsten/texifyidea/util/files/ReferencedFileSetCache.kt
Expand Up @@ -31,6 +31,8 @@ class ReferencedFileSetCache {
*/
private val fileSetCache = ConcurrentHashMap<VirtualFile, Set<PsiFile>>()

private val rootFilesCache = 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
Expand All @@ -46,6 +48,31 @@ class ReferencedFileSetCache {
*/
@Synchronized
fun fileSetFor(file: PsiFile): Set<PsiFile> {
return getSetFromCache(file, fileSetCache) {
findReferencedFileSetWithoutCache()
}
}

fun rootFilesFor(file: PsiFile): Set<PsiFile> {
return getSetFromCache(file, rootFilesCache) {
findRootFilesWithoutCache()
}
}

/**
* Clears the cache for base file `file`.
*/
fun dropCaches(file: VirtualFile) {
fileSetCache.remove(file)
rootFilesCache.remove(file)
}

fun dropAllCaches() {
fileSetCache.keys.forEach { fileSetCache.remove(it) }
rootFilesCache.keys.forEach { rootFilesCache.remove(it) }
}

private fun getSetFromCache(file: PsiFile, cache: ConcurrentHashMap<VirtualFile, Set<PsiFile>>, updateFunction: PsiFile.() -> Set<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) {
Expand All @@ -62,28 +89,17 @@ class ReferencedFileSetCache {
// 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) {
if (!cache.containsKey(file.virtualFile) || numberOfIncludesChanged) {
runReadAction {
fileSetCache[file.virtualFile] = findReferencedFileSetWithoutCache(file)
cache[file.virtualFile] = file.updateFunction()
}
}
}
}
fileSetCache[file.virtualFile] ?: setOf(file)
cache[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) }
}
}
Expand Up @@ -22,6 +22,8 @@ interface ReferencedFileSetService {
*/
fun referencedFileSetOf(psiFile: PsiFile): Set<PsiFile>

fun rootFilesOf(psiFile: PsiFile): Set<PsiFile>

/**
* Invalidates the caches for the given file.
*/
Expand Down
39 changes: 31 additions & 8 deletions src/nl/hannahsten/texifyidea/util/files/RootFile.kt
Expand Up @@ -10,23 +10,28 @@ 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.psi.LatexEnvironment
import nl.hannahsten.texifyidea.run.latex.LatexRunConfiguration
import java.util.HashMap
import nl.hannahsten.texifyidea.util.firstChildOfType
import java.util.*

/**
* Scans all file inclusions and finds the file that is at the base of all inclusions.
* Scans all file inclusions and finds the files that are at the base of all inclusions.
* Note that this can be multiple files.
*
* When no file is included, `this` file will be returned.
*/
fun PsiFile.findRootFile(): PsiFile {
fun PsiFile.findRootFilesWithoutCache(): Set<PsiFile> {
val magicComment = magicComment()
val roots = mutableSetOf<PsiFile>()

if (magicComment.contains(DefaultMagicKeys.ROOT)) {
val path = magicComment.value(DefaultMagicKeys.ROOT) ?: ""
this.findFile(path)?.let { return it }
this.findFile(path)?.let { roots.add(it) }
}

if (this.isRoot()) {
return this
roots.add(this)
}

// We need to scan all file inclusions in the project, because any file could include the current file
Expand All @@ -46,13 +51,28 @@ fun PsiFile.findRootFile(): PsiFile {

// If the root file contains this, we have found the root file
if (file.contains(this, inclusions)) {
return file
roots.add(file)
}
}

return this
return if (roots.isEmpty()) setOf(this) else roots
}

/**
* Gets the first file from the root files found by [findRootFilesWithoutCache] which is stored in the cache.
*
* As a best guess, get the first of the root files returned by [findRootFiles].
*
* Note: LaTeX Files can have more than one * root file, so using [findRootFiles] and explicitly handling the cases of
* multiple root files is preferred over using [findRootFile].
*/
fun PsiFile.findRootFile(): PsiFile = findRootFiles().firstOrNull() ?: this

/**
* Gets the set of files that are the root files of `this` file.
*/
fun PsiFile.findRootFiles(): Set<PsiFile> = ReferencedFileSetService.getInstance().rootFilesOf(this)

/**
* Checks if the given file is included by `this` file.
*
Expand Down Expand Up @@ -132,15 +152,18 @@ fun PsiFile.isRoot(): Boolean {
// Function to avoid unnecessary evaluation
fun documentClass() = this.commandsInFile().find { it.commandToken.text == "\\documentclass" }

fun documentEnvironment() = this.firstChildOfType(LatexEnvironment::class)?.environmentName == "document"

// 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
if (usesSubFiles()) return false

// 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()
return isMainFileInAnyConfiguration || documentEnvironment()
}

/**
Expand Down
Expand Up @@ -14,6 +14,8 @@ class ReferencedFileSetServiceImpl : ReferencedFileSetService {

override fun referencedFileSetOf(psiFile: PsiFile) = cache.fileSetFor(psiFile)

override fun rootFilesOf(psiFile: PsiFile): Set<PsiFile> = cache.rootFilesFor(psiFile)

override fun dropCaches(file: VirtualFile) = cache.dropCaches(file)

override fun dropAllCaches() = cache.dropAllCaches()
Expand Down
@@ -0,0 +1,29 @@
package nl.hannahsten.texifyidea.inspections.latex

import nl.hannahsten.texifyidea.file.LatexFileType
import nl.hannahsten.texifyidea.inspections.TexifyInspectionTestBase

class LatexDocumentclassNotInRootInspectionTest : TexifyInspectionTestBase(LatexDocumentclassNotInRootInspection()) {
override fun getTestDataPath(): String {
return "test/resources/inspections/latex/documentclassnotinroot"
}

fun testNoWarning() {
myFixture.configureByText(
LatexFileType,
"""
\documentclass{article}

\begin{document}
bla
\end{document}
""".trimIndent()
)
myFixture.checkHighlighting()
}

fun testWarning() {
myFixture.configureByFiles("preamble.sty", "main.tex")
myFixture.checkHighlighting()
}
}
@@ -0,0 +1,5 @@
\usepackage{preamble}

\begin{document}
Bla.
\end{document}
@@ -0,0 +1,5 @@
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{preamble}

<warning>\documentclass{article}</warning>