Skip to content

Commit a87d840

Browse files
authored
Merge pull request #3828 from Hannah-Sten/tectonic-toml
Add support for Tectonic.toml
2 parents 15c8d09 + 280947f commit a87d840

File tree

10 files changed

+119
-55
lines changed

10 files changed

+119
-55
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
## [Unreleased]
44

55
### Added
6+
* Support Tectonic V2 CLI in run configuration
7+
* Add basic support for multiple inputs in Tectonic.toml
8+
* Improve performance of file set cache used by inspections
69
* Support label references to user defined listings environment
710
* Add option to disable automatic compilation in power save mode
811
* Convert automatic compilation settings to a combobox

Writerside/topics/Run-configuration-settings.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ _Since b0.6.6_
9292

9393
See [https://tectonic-typesetting.github.io/en-US/](https://tectonic-typesetting.github.io/en-US/) for installation and more info.
9494
Tectonic has the advantage that it downloads packages automatically, compiles just as much times as needed and handles BibTeX, but it often only works for not too complicated LaTeX documents.
95-
9695
It also has automatic compilation using `tectonic -X watch`.
9796

97+
There is some basic support for a `Tectonic.toml` file, including inspection support (missing imports, for example) for multiple inputs in the toml file (Tectonic 0.15.1 or later).
98+
9899
The documentation can be found at [https://tectonic-typesetting.github.io/book/latest/](https://tectonic-typesetting.github.io/book/latest/)
99100

100101
## BibTeX compilers

build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ dependencies {
140140
// Parsing xml
141141
implementation("com.fasterxml.jackson.core:jackson-core:2.18.2")
142142
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.18.2")
143+
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.18.2")
143144
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
144145

145146
// Http requests

src/nl/hannahsten/texifyidea/run/compiler/LatexCompiler.kt

+14-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import nl.hannahsten.texifyidea.settings.sdk.DockerSdk
1212
import nl.hannahsten.texifyidea.settings.sdk.DockerSdkAdditionalData
1313
import nl.hannahsten.texifyidea.settings.sdk.LatexSdkUtil
1414
import nl.hannahsten.texifyidea.util.LatexmkRcFileFinder
15+
import nl.hannahsten.texifyidea.util.files.hasTectonicTomlFile
1516
import nl.hannahsten.texifyidea.util.runCommand
1617
import java.util.*
1718

@@ -231,15 +232,22 @@ enum class LatexCompiler(private val displayName: String, val executableName: St
231232
moduleRoot: VirtualFile?,
232233
moduleRoots: Array<VirtualFile>
233234
): MutableList<String> {
234-
// The available command line arguments can be found at https://github.com/tectonic-typesetting/tectonic/blob/d7a8497c90deb08b5e5792a11d6e8b082f53bbb7/src/bin/tectonic.rs#L158
235235
val command = mutableListOf(runConfig.compilerPath ?: executableName)
236236

237-
command.add("--synctex")
237+
// The available command line arguments can be found at https://github.com/tectonic-typesetting/tectonic/blob/d7a8497c90deb08b5e5792a11d6e8b082f53bbb7/src/bin/tectonic.rs#L158
238+
// The V2 CLI uses a toml file and should not have arguments
239+
if (runConfig.mainFile?.hasTectonicTomlFile() != true) {
240+
command.add("--synctex")
238241

239-
command.add("--outfmt=${runConfig.outputFormat.name.lowercase(Locale.getDefault())}")
242+
command.add("--outfmt=${runConfig.outputFormat.name.lowercase(Locale.getDefault())}")
240243

241-
if (outputPath != null) {
242-
command.add("--outdir=$outputPath")
244+
if (outputPath != null) {
245+
command.add("--outdir=$outputPath")
246+
}
247+
}
248+
else {
249+
command.add("-X")
250+
command.add("build")
243251
}
244252

245253
return command
@@ -364,7 +372,7 @@ enum class LatexCompiler(private val displayName: String, val executableName: St
364372
command.add(runConfig.beforeRunCommand + " \\input{${mainFile.name}}")
365373
}
366374
}
367-
else {
375+
else if (runConfig.compiler != TECTONIC || runConfig.mainFile?.hasTectonicTomlFile() != true) {
368376
command.add(mainFile.name)
369377
}
370378

src/nl/hannahsten/texifyidea/run/latex/LatexCommandLineState.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import nl.hannahsten.texifyidea.run.pdfviewer.ExternalPdfViewer
2727
import nl.hannahsten.texifyidea.run.sumatra.SumatraAvailabilityChecker
2828
import nl.hannahsten.texifyidea.run.sumatra.SumatraForwardSearchListener
2929
import nl.hannahsten.texifyidea.util.files.commandsInFileSet
30+
import nl.hannahsten.texifyidea.util.files.findTectonicTomlFile
31+
import nl.hannahsten.texifyidea.util.files.hasTectonicTomlFile
3032
import nl.hannahsten.texifyidea.util.files.psiFile
3133
import nl.hannahsten.texifyidea.util.includedPackages
3234
import nl.hannahsten.texifyidea.util.magic.PackageMagic
@@ -92,7 +94,8 @@ open class LatexCommandLineState(environment: ExecutionEnvironment, private val
9294
val command: List<String> = compiler.getCommand(runConfig, environment.project)
9395
?: throw ExecutionException("Compile command could not be created.")
9496

95-
val commandLine = GeneralCommandLine(command).withWorkDirectory(mainFile.parent.path)
97+
val workingDirectory = if (compiler == LatexCompiler.TECTONIC && mainFile.hasTectonicTomlFile()) mainFile.findTectonicTomlFile()!!.parent.path else mainFile.parent.path
98+
val commandLine = GeneralCommandLine(command).withWorkDirectory(workingDirectory)
9699
.withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
97100
.withEnvironment(runConfig.environmentVariables.envs)
98101
val handler = KillableProcessHandler(commandLine)

src/nl/hannahsten/texifyidea/util/files/FileSet.kt

+57-24
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package nl.hannahsten.texifyidea.util.files
22

3+
import com.fasterxml.jackson.dataformat.toml.TomlMapper
34
import com.intellij.openapi.application.runReadAction
45
import com.intellij.openapi.project.Project
5-
import com.intellij.openapi.vfs.LocalFileSystem
6+
import com.intellij.openapi.roots.ProjectFileIndex
67
import com.intellij.openapi.vfs.VirtualFile
8+
import com.intellij.openapi.vfs.findFile
9+
import com.intellij.openapi.vfs.LocalFileSystem
710
import com.intellij.psi.PsiFile
811
import com.intellij.psi.search.GlobalSearchScope
912
import nl.hannahsten.texifyidea.index.BibtexEntryIndex
@@ -17,51 +20,81 @@ import nl.hannahsten.texifyidea.util.magic.CommandMagic
1720
import nl.hannahsten.texifyidea.util.magic.cmd
1821
import nl.hannahsten.texifyidea.util.parser.isDefinition
1922
import nl.hannahsten.texifyidea.util.parser.requiredParameter
23+
import java.io.File
2024

2125
/**
2226
* Finds all the files in the project that are somehow related using includes.
2327
*
2428
* When A includes B and B includes C then A, B & C will all return a set containing A, B & C.
29+
* There can be multiple root files in one file set.
2530
*
2631
* Be careful when using this function directly over something like [ReferencedFileSetService] where the result
2732
* values are cached.
2833
*
29-
* @receiver The file to find the reference set of.
30-
* @return All the LaTeX and BibTeX files that are cross referenced between each other.
34+
* @return Map all root files which include any other file, to the file set containing that root file.
3135
*/
3236
// Internal because only ReferencedFileSetCache should call this
33-
internal fun PsiFile.findReferencedFileSetWithoutCache(): Set<PsiFile> {
34-
// Setup.
35-
val project = this.project
36-
val includes = LatexIncludesIndex.Util.getItems(project)
37-
37+
internal fun Project.findReferencedFileSetWithoutCache(): Map<PsiFile, Set<PsiFile>> {
3838
// Find all root files.
39-
val roots = includes.asSequence()
39+
return LatexIncludesIndex.Util.getItems(this)
40+
.asSequence()
4041
.map { it.containingFile }
4142
.distinct()
4243
.filter { it.isRoot() }
4344
.toSet()
44-
45-
// Map root to all directly referenced files.
46-
val sets = HashMap<PsiFile, Set<PsiFile>>()
47-
for (root in roots) {
48-
val referenced = runReadAction { root.referencedFiles(root.virtualFile) } + root
49-
50-
if (referenced.contains(this)) {
51-
return referenced + this
45+
.associateWith { root ->
46+
// Map root to all directly referenced files.
47+
runReadAction { root.referencedFiles(root.virtualFile) } + root
5248
}
49+
}
5350

54-
sets[root] = referenced
51+
/**
52+
* Check for tectonic.toml files in the project.
53+
* These files can input multiple tex files, which would then be in the same file set.
54+
* Example file: https://github.com/Hannah-Sten/TeXiFy-IDEA/issues/3773#issuecomment-2503221732
55+
* @return List of sets of files included by the same toml file.
56+
*/
57+
fun findTectonicTomlInclusions(project: Project): List<Set<PsiFile>> {
58+
// Actually, according to https://tectonic-typesetting.github.io/book/latest/v2cli/build.html?highlight=tectonic.toml#remarks Tectonic.toml files can appear in any parent directory, but we only search in the project for now
59+
val tomlFiles = findTectonicTomlFiles(project)
60+
val filesets = tomlFiles.mapNotNull { tomlFile ->
61+
val data = TomlMapper().readValue(File(tomlFile.path), Map::class.java)
62+
val outputList = data.getOrDefault("output", null) as? List<*> ?: return@mapNotNull null
63+
val inputs = (outputList.firstOrNull() as? Map<*, *>)?.getOrDefault("inputs", null) as? List<*> ?: return@mapNotNull null
64+
// Inputs can be either a map "inline" -> String or file name
65+
// Actually it can also be just a single file name, but then we don't need all this gymnastics
66+
inputs.filterIsInstance<String>().mapNotNull {
67+
tomlFile.parent.findFile("src/$it")?.psiFile(project)
68+
}.toSet()
5569
}
5670

57-
// Look for matching root.
58-
for (referenced in sets.values) {
59-
if (referenced.contains(this)) {
60-
return referenced + this
71+
return filesets
72+
}
73+
74+
private fun findTectonicTomlFiles(project: Project): MutableSet<VirtualFile> {
75+
val tomlFiles = mutableSetOf<VirtualFile>()
76+
ProjectFileIndex.getInstance(project).iterateContent({ tomlFiles.add(it) }, { it.name == "Tectonic.toml" })
77+
return tomlFiles
78+
}
79+
80+
/**
81+
* A toml file can be in any parent directory.
82+
*/
83+
fun VirtualFile.hasTectonicTomlFile() = findTectonicTomlFile() != null
84+
85+
fun VirtualFile.findTectonicTomlFile(): VirtualFile? {
86+
var parent = this
87+
for (i in 0..20) {
88+
if (parent.parent != null && parent.parent.isDirectory && parent.parent.exists()) {
89+
parent = parent.parent
90+
}
91+
else {
92+
break
6193
}
62-
}
6394

64-
return setOf(this)
95+
parent?.findFile("Tectonic.toml")?.let { return it }
96+
}
97+
return null
6598
}
6699

67100
/**

src/nl/hannahsten/texifyidea/util/files/ReferencedFileSetCache.kt

+22-14
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,27 @@ class ReferencedFileSetCache {
8383
* once and then fill both caches with all the information we have.
8484
*/
8585
private fun updateCachesFor(requestedFile: PsiFile) {
86-
val fileset = requestedFile.findReferencedFileSetWithoutCache()
87-
for (file in fileset) {
88-
fileSetCache[file.virtualFile] = fileset.map { it.createSmartPointer() }.toSet()
86+
val filesets = requestedFile.project.findReferencedFileSetWithoutCache().toMutableMap()
87+
val tectonicInclusions = findTectonicTomlInclusions(requestedFile.project)
88+
89+
// Now we join all the file sets that are in the same file set according to the Tectonic.toml file
90+
for (inclusionsSet in tectonicInclusions) {
91+
val mappings = filesets.filter { it.value.intersect(inclusionsSet).isNotEmpty() }
92+
val newFileSet = mappings.values.flatten().toSet() + inclusionsSet
93+
mappings.forEach {
94+
filesets[it.key] = newFileSet
95+
}
8996
}
9097

91-
val rootfiles = requestedFile.findRootFilesWithoutCache(fileset)
92-
for (file in fileset) {
93-
rootFilesCache[file.virtualFile] = rootfiles.map { it.createSmartPointer() }.toSet()
98+
for (fileset in filesets.values) {
99+
for (file in fileset) {
100+
fileSetCache[file.virtualFile] = fileset.map { it.createSmartPointer() }.toSet()
101+
}
102+
103+
val rootfiles = requestedFile.findRootFilesWithoutCache(fileset)
104+
for (file in fileset) {
105+
rootFilesCache[file.virtualFile] = rootfiles.map { it.createSmartPointer() }.toSet()
106+
}
94107
}
95108
}
96109

@@ -108,16 +121,11 @@ class ReferencedFileSetCache {
108121
// Use the keys of the whole project, because suppose a new include includes the current file, it could be anywhere in the project
109122
// Note that LatexIncludesIndex.Util.getItems(file.project) may be a slow operation and should not be run on EDT
110123
val includes = LatexIncludesIndex.Util.getItems(file.project)
111-
val numberOfIncludesChanged = if (includes.size != numberOfIncludes[file.project]) {
124+
125+
// The cache should be complete once filled, any files not in there are assumed to not be part of a file set that has a valid root file
126+
if (includes.size != numberOfIncludes[file.project]) {
112127
numberOfIncludes[file.project] = includes.size
113128
dropAllCaches()
114-
true
115-
}
116-
else {
117-
false
118-
}
119-
120-
if (!cache.containsKey(file.virtualFile) || numberOfIncludesChanged) {
121129
updateCachesFor(file)
122130
}
123131
}

test/nl/hannahsten/texifyidea/inspections/bibtex/BibtexUnusedEntryInspectionTest.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package nl.hannahsten.texifyidea.inspections.bibtex
22

33
import nl.hannahsten.texifyidea.inspections.TexifyInspectionTestBase
44
import nl.hannahsten.texifyidea.testutils.writeCommand
5+
import nl.hannahsten.texifyidea.util.files.ReferencedFileSetService
56

67
class BibtexUnusedEntryInspectionTest : TexifyInspectionTestBase(BibtexUnusedEntryInspection()) {
78

@@ -15,7 +16,10 @@ class BibtexUnusedEntryInspectionTest : TexifyInspectionTestBase(BibtexUnusedEnt
1516
}
1617

1718
fun `test quick fix`() {
18-
myFixture.configureByFiles("references-before.bib", "main-quick-fix.tex")
19+
myFixture.configureByFiles("references-before.bib", "main-quick-fix.tex").forEach {
20+
// Refresh cache
21+
ReferencedFileSetService.getInstance().referencedFileSetOf(it)
22+
}
1923
val quickFixes = myFixture.getAllQuickFixes()
2024
assertEquals("Expected number of quick fixes:", 2, quickFixes.size)
2125
writeCommand(myFixture.project) {

test/nl/hannahsten/texifyidea/reference/BibtexIdCompletionTest.kt

+6-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package nl.hannahsten.texifyidea.reference
33
import com.intellij.codeInsight.completion.CompletionType
44
import com.intellij.codeInsight.documentation.DocumentationManager
55
import com.intellij.testFramework.fixtures.BasePlatformTestCase
6-
import org.junit.Test
6+
import nl.hannahsten.texifyidea.util.files.ReferencedFileSetService
77

88
class BibtexIdCompletionTest : BasePlatformTestCase() {
99

@@ -16,7 +16,6 @@ class BibtexIdCompletionTest : BasePlatformTestCase() {
1616
super.setUp()
1717
}
1818

19-
@Test
2019
fun testCompleteLatexReferences() {
2120
// when
2221
runCompletion()
@@ -30,7 +29,6 @@ class BibtexIdCompletionTest : BasePlatformTestCase() {
3029
assertTrue(entry1.allLookupStrings.contains("{Missing the Point(er): On the Effectiveness of Code Pointer Integrity}"))
3130
}
3231

33-
@Test
3432
fun testCompletionResultsLowerCase() {
3533
// when
3634
runCompletion()
@@ -41,7 +39,6 @@ class BibtexIdCompletionTest : BasePlatformTestCase() {
4139
assertTrue(result?.contains("Muchnick1997") == true)
4240
}
4341

44-
@Test
4542
fun testCompletionResultsSecondEntry() {
4643
// when
4744
runCompletion()
@@ -54,14 +51,12 @@ class BibtexIdCompletionTest : BasePlatformTestCase() {
5451
assertTrue(result?.contains("Burow2016") == true)
5552
}
5653

57-
@Test
5854
fun testCompleteBibtexWithCorrectCase() {
5955
// Using the following failed sometimes
6056
val testName = getTestName(false)
6157
myFixture.testCompletion("${testName}_before.tex", "${testName}_after.tex", "$testName.bib")
6258
}
6359

64-
@Test
6560
fun testBibtexEntryDocumentation() {
6661
runCompletion()
6762
val element = DocumentationManager.getInstance(myFixture.project).getElementFromLookup(myFixture.editor, myFixture.file)
@@ -78,7 +73,11 @@ class BibtexIdCompletionTest : BasePlatformTestCase() {
7873
}
7974

8075
private fun runCompletion() {
81-
myFixture.configureByFiles("${getTestName(false)}.tex", "bibtex.bib")
76+
val files = myFixture.configureByFiles("${getTestName(false)}.tex", "bibtex.bib")
77+
// The first time completion runs, due to caching there may be a race condition
78+
for (file in files) {
79+
ReferencedFileSetService.getInstance().referencedFileSetOf(file)
80+
}
8281
// when
8382
myFixture.complete(CompletionType.BASIC)
8483
}

test/nl/hannahsten/texifyidea/reference/BibtexIdRemoteLibraryCompletionTest.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import nl.hannahsten.texifyidea.remotelibraries.RemoteLibraryManager
88
import nl.hannahsten.texifyidea.remotelibraries.state.BibtexEntryListConverter
99
import nl.hannahsten.texifyidea.remotelibraries.state.LibraryState
1010
import nl.hannahsten.texifyidea.remotelibraries.zotero.ZoteroLibrary
11+
import nl.hannahsten.texifyidea.util.files.ReferencedFileSetService
1112

1213
class BibtexIdRemoteLibraryCompletionTest : BasePlatformTestCase() {
1314

@@ -101,7 +102,10 @@ class BibtexIdRemoteLibraryCompletionTest : BasePlatformTestCase() {
101102
mockkObject(RemoteLibraryManager)
102103
every { RemoteLibraryManager.getInstance().getLibraries() } returns mutableMapOf("aaa" to LibraryState("mocked", ZoteroLibrary::class.java, BibtexEntryListConverter().fromString(remoteBib), "test url"))
103104

104-
myFixture.configureByFiles("$path/before.tex", "$path/bibtex_before.bib")
105+
myFixture.configureByFiles("$path/before.tex", "$path/bibtex_before.bib").forEach {
106+
// Refresh cache
107+
ReferencedFileSetService.getInstance().referencedFileSetOf(it)
108+
}
105109

106110
myFixture.complete(CompletionType.BASIC)
107111

0 commit comments

Comments
 (0)