mirror of
https://github.com/latex-lsp/texlab.git
synced 2025-12-23 09:19:21 +00:00
Resolve included files
This commit is contained in:
parent
9b8ece8f67
commit
b3eace804a
9 changed files with 266 additions and 39 deletions
|
|
@ -5,16 +5,15 @@ import java.net.URI
|
|||
|
||||
abstract class Document(val uri: URI) {
|
||||
|
||||
var version: Int = -1
|
||||
private set
|
||||
private var version: Int = -1
|
||||
|
||||
var text: String = ""
|
||||
private set
|
||||
|
||||
protected abstract fun analyze()
|
||||
val isFile: Boolean = uri.scheme == "file"
|
||||
|
||||
fun update(changes: List<TextDocumentContentChangeEvent>, version: Int) {
|
||||
if (version > this.version) {
|
||||
if (this.version > version) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -33,4 +32,6 @@ abstract class Document(val uri: URI) {
|
|||
this.version = version
|
||||
analyze()
|
||||
}
|
||||
|
||||
protected abstract fun analyze()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ class LanguageServerImpl : LanguageServer, LanguageClientAware {
|
|||
|
||||
override fun initialize(params: InitializeParams): CompletableFuture<InitializeResult> {
|
||||
return CompletableFuture.supplyAsync {
|
||||
val root = URI.create(params.rootUri)
|
||||
synchronized(workspace) {
|
||||
loadWorkspace(root)
|
||||
}
|
||||
|
||||
val capabilities = ServerCapabilities().apply {
|
||||
val syncOptions = TextDocumentSyncOptions().apply {
|
||||
openClose = true
|
||||
|
|
@ -31,11 +36,6 @@ class LanguageServerImpl : LanguageServer, LanguageClientAware {
|
|||
}
|
||||
textDocumentSync = Either.forRight(syncOptions)
|
||||
}
|
||||
|
||||
val root = URI.create(params.rootUri)
|
||||
synchronized(workspace) {
|
||||
loadWorkspace(root)
|
||||
}
|
||||
InitializeResult(capabilities)
|
||||
}
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ class LanguageServerImpl : LanguageServer, LanguageClientAware {
|
|||
val language = getLanguageByExtension(extension) ?: return
|
||||
try {
|
||||
val text = Files.readAllBytes(file).toString(Charsets.UTF_8)
|
||||
workspace.create(file.toUri(), text, language)
|
||||
workspace.create(file.toUri(), language, text)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class TextDocumentServiceImpl(private val workspace: Workspace) : TextDocumentSe
|
|||
params.textDocument.apply {
|
||||
val language = getLanguageById(languageId) ?: return
|
||||
synchronized(workspace) {
|
||||
workspace.create(URI.create(uri), text, language)
|
||||
workspace.create(URI.create(uri), language, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,16 @@
|
|||
package texlab
|
||||
|
||||
import org.eclipse.lsp4j.TextDocumentContentChangeEvent
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.PathMatcher
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
|
||||
class Workspace {
|
||||
private val documents = mutableListOf<Document>()
|
||||
private val matcher: PathMatcher = FileSystems.getDefault().getPathMatcher("glob:**.{tex,sty,cls,bib}")
|
||||
private val extensions = arrayOf(".tex", ".sty", ".cls", ".bib")
|
||||
|
||||
fun initialize(directory: Path) {
|
||||
Files.walk(directory)
|
||||
.filter { Files.isRegularFile(it) }
|
||||
.filter { matcher.matches(it) }
|
||||
.forEach {
|
||||
val extension = it.fileName.toFile().extension
|
||||
val language = getLanguageByExtension(extension) ?: return@forEach
|
||||
try {
|
||||
val text = Files.readAllBytes(it).toString(Charsets.UTF_8)
|
||||
create(it.toUri(), text, language)
|
||||
} catch (e: IOException) {
|
||||
// TODO: Log this error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun create(uri: URI, text: String, language: Language) {
|
||||
fun create(uri: URI, language: Language, text: String) {
|
||||
var document = documents.firstOrNull { it.uri == uri }
|
||||
if (document == null) {
|
||||
document = when (language) {
|
||||
|
|
@ -45,7 +27,56 @@ class Workspace {
|
|||
}
|
||||
|
||||
fun update(uri: URI, changes: List<TextDocumentContentChangeEvent>, version: Int) {
|
||||
val document = documents.firstOrNull { it.uri == uri } ?: return
|
||||
document.update(changes, version)
|
||||
documents.firstOrNull { it.uri == uri }
|
||||
?.update(changes, version)
|
||||
}
|
||||
|
||||
fun resolve(uri: URI, relativePath: String): Document? {
|
||||
fun find(path: String): Document? {
|
||||
return documents
|
||||
.filter { it.isFile }
|
||||
.firstOrNull { it.uri.path == path }
|
||||
}
|
||||
|
||||
return try {
|
||||
val basePath = Paths.get(uri.path).parent
|
||||
val fullPath = basePath.resolve(relativePath).toString().replace('\\', '/')
|
||||
var document = find(fullPath)
|
||||
extensions.forEach { document = document ?: find("$fullPath$it") }
|
||||
return document
|
||||
} catch (e: InvalidPathException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun relatedDocuments(uri: URI): List<Document> {
|
||||
val edges = mutableSetOf<Pair<Document, Document>>()
|
||||
documents.filterIsInstance<LatexDocument>()
|
||||
.filter { it.isFile }
|
||||
.forEach { parent ->
|
||||
parent.tree.includes
|
||||
.mapNotNull { resolve(parent.uri, it.path) }
|
||||
.forEach { child ->
|
||||
edges.add(Pair(parent, child))
|
||||
edges.add(Pair(child, parent))
|
||||
}
|
||||
}
|
||||
|
||||
val results = mutableListOf<Document>()
|
||||
val start = documents.firstOrNull { it.uri == uri } ?: return results
|
||||
val visited = mutableSetOf<Document>()
|
||||
val stack = Stack<Document>()
|
||||
stack.push(start)
|
||||
while (!stack.empty()) {
|
||||
val current = stack.pop()
|
||||
if (!visited.add(current)) {
|
||||
continue
|
||||
}
|
||||
|
||||
results.add(current)
|
||||
documents.filter { edges.contains(Pair(current, it)) }
|
||||
.forEach { stack.push(it) }
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/main/kotlin/texlab/latex/LatexInclude.kt
Normal file
21
src/main/kotlin/texlab/latex/LatexInclude.kt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package texlab.latex
|
||||
|
||||
data class LatexInclude(val command: LatexCommandSyntax, val path: String) {
|
||||
companion object {
|
||||
private val COMMAND_NAMES =
|
||||
arrayOf("\\include", "\\input", "\\bibliography", "\\addbibresource", "\\usepackage")
|
||||
|
||||
fun analyze(root: LatexSyntaxNode): List<LatexInclude> {
|
||||
return root.descendants()
|
||||
.filterIsInstance<LatexCommandSyntax>()
|
||||
.filter { COMMAND_NAMES.contains(it.name.text) }
|
||||
.mapNotNull { analyze(it) }
|
||||
}
|
||||
|
||||
private fun analyze(command: LatexCommandSyntax): LatexInclude? {
|
||||
val text = command.extractText(0) ?: return null
|
||||
val path = text.words.joinToString(" ") { it.text }
|
||||
return LatexInclude(command, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,29 @@ sealed class LatexSyntaxNode {
|
|||
|
||||
val end: Position
|
||||
get() = range.end
|
||||
|
||||
fun descendants(): List<LatexSyntaxNode> {
|
||||
val results = mutableListOf<LatexSyntaxNode>()
|
||||
|
||||
fun visit(node: LatexSyntaxNode) {
|
||||
results.add(node)
|
||||
when (node) {
|
||||
is LatexDocumentSyntax -> {
|
||||
node.children.forEach { visit(it) }
|
||||
}
|
||||
is LatexGroupSyntax -> {
|
||||
node.children.forEach { visit(it) }
|
||||
}
|
||||
is LatexCommandSyntax -> {
|
||||
node.options?.also { visit(it) }
|
||||
node.args.forEach { visit(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit(this)
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
data class LatexDocumentSyntax(override val range: Range,
|
||||
|
|
@ -24,7 +47,31 @@ data class LatexGroupSyntax(override val range: Range,
|
|||
data class LatexCommandSyntax(override val range: Range,
|
||||
val name: LatexToken,
|
||||
val options: LatexGroupSyntax?,
|
||||
val args: List<LatexGroupSyntax>) : LatexSyntaxNode()
|
||||
val args: List<LatexGroupSyntax>) : LatexSyntaxNode() {
|
||||
|
||||
fun extractText(index: Int): LatexTextSyntax? {
|
||||
return if (args.size > index && args[index].children.size == 1) {
|
||||
val child = args[index].children[0]
|
||||
if (child is LatexTextSyntax) {
|
||||
child
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun extractWord(index: Int): String? {
|
||||
val text = extractText(index)
|
||||
return if (text == null || text.words.size != 1) {
|
||||
null
|
||||
} else {
|
||||
text.words[0].text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class LatexTextSyntax(override val range: Range,
|
||||
val words: List<LatexToken>) : LatexSyntaxNode()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package texlab.latex
|
||||
|
||||
class LatexSyntaxTree(private val root: LatexDocumentSyntax) {
|
||||
class LatexSyntaxTree(text: String) {
|
||||
|
||||
constructor(text: String) : this(LatexParser.parse(text))
|
||||
private val root: LatexDocumentSyntax = LatexParser.parse(text)
|
||||
|
||||
val includes: List<LatexInclude> = LatexInclude.analyze(root)
|
||||
}
|
||||
|
|
|
|||
77
src/test/kotlin/texlab/WorkspaceTests.kt
Normal file
77
src/test/kotlin/texlab/WorkspaceTests.kt
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package texlab
|
||||
|
||||
import org.eclipse.lsp4j.Position
|
||||
import org.eclipse.lsp4j.Range
|
||||
import org.eclipse.lsp4j.TextDocumentContentChangeEvent
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.URI
|
||||
|
||||
class WorkspaceTests {
|
||||
private val document1 = URI.create("file:///foo/bar.tex")
|
||||
private val document2 = URI.create("file:///foo/baz/qux.tex")
|
||||
private val document3 = URI.create("file:///foo/baz.bib")
|
||||
|
||||
private fun Workspace.verifyRelatedDocuments(expected: List<URI>) {
|
||||
val actual = relatedDocuments(expected[0]).map { it.uri }
|
||||
assertEquals(expected.size, actual.size)
|
||||
expected.zip(actual)
|
||||
.forEach { assertEquals(it.first, it.second) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should find related bibliographies`() {
|
||||
val workspace = Workspace()
|
||||
workspace.create(document1, Language.LATEX, "\\bibliography{baz.bib}")
|
||||
workspace.create(document2, Language.LATEX, "")
|
||||
workspace.create(document3, Language.BIBTEX, "")
|
||||
workspace.verifyRelatedDocuments(listOf(document3, document1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should append extensions when finding related documents`() {
|
||||
val workspace = Workspace()
|
||||
workspace.create(document1, Language.LATEX, "\\input{baz/qux}")
|
||||
workspace.create(document2, Language.LATEX, "")
|
||||
workspace.verifyRelatedDocuments(listOf(document1, document2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should not crash when including invalid paths`() {
|
||||
val workspace = Workspace()
|
||||
workspace.create(document1, Language.LATEX, "\\include{<foo>?|bar|:}")
|
||||
workspace.verifyRelatedDocuments(listOf(document1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should not crash when including empty paths`() {
|
||||
val workspace = Workspace()
|
||||
workspace.create(document1, Language.LATEX, "\\include{}")
|
||||
workspace.verifyRelatedDocuments(listOf(document1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should ignore paths that cannot be resolved`() {
|
||||
val workspace = Workspace()
|
||||
workspace.create(document1, Language.LATEX, "\\addbibresource{bar}")
|
||||
workspace.verifyRelatedDocuments(listOf(document1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should handle include cycles`() {
|
||||
val workspace = Workspace()
|
||||
workspace.create(document1, Language.LATEX, "\\include{baz/qux}");
|
||||
workspace.create(document2, Language.LATEX, "\\include{../bar.tex}");
|
||||
workspace.verifyRelatedDocuments(listOf(document1, document2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should handle incremental updates`() {
|
||||
val workspace = Workspace()
|
||||
workspace.create(document1, Language.LATEX, "foo\nbar")
|
||||
val range = Range(Position(0, 0), Position(0, 3))
|
||||
val change = TextDocumentContentChangeEvent(range, 3, "baz")
|
||||
workspace.update(document1, listOf(change), 1)
|
||||
assertEquals("baz\nbar", workspace.relatedDocuments(document1)[0].text)
|
||||
}
|
||||
}
|
||||
48
src/test/kotlin/texlab/latex/LatexIncludeTests.kt
Normal file
48
src/test/kotlin/texlab/latex/LatexIncludeTests.kt
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package texlab.latex
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class LatexIncludeTests {
|
||||
|
||||
@Test
|
||||
fun `it should find includes`() {
|
||||
val text = "\\include{foo}\n\\input{bar/qux}"
|
||||
val root = LatexParser.parse(text)
|
||||
val include1 = LatexInclude(root.children[0] as LatexCommandSyntax, "foo")
|
||||
val include2 = LatexInclude(root.children[1] as LatexCommandSyntax, "bar/qux")
|
||||
val includes = LatexInclude.analyze(root)
|
||||
assertEquals(2, includes.size)
|
||||
assertEquals(include1, includes[0])
|
||||
assertEquals(include2, includes[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should find bibliographies`() {
|
||||
val text = "\\addbibresource{foo}\n\\bibliography{bar}"
|
||||
val root = LatexParser.parse(text)
|
||||
val include1 = LatexInclude(root.children[0] as LatexCommandSyntax, "foo")
|
||||
val include2 = LatexInclude(root.children[1] as LatexCommandSyntax, "bar")
|
||||
val includes = LatexInclude.analyze(root)
|
||||
assertEquals(2, includes.size)
|
||||
assertEquals(include1, includes[0])
|
||||
assertEquals(include2, includes[1])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should handle paths with spaces`() {
|
||||
val text = "\\include{foo bar.tex}"
|
||||
val root = LatexParser.parse(text)
|
||||
val expected = LatexInclude(root.children[0] as LatexCommandSyntax, "foo bar.tex")
|
||||
val includes = LatexInclude.analyze(root)
|
||||
assertEquals(1, includes.size)
|
||||
assertEquals(expected, includes[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should ignore invalid commands`() {
|
||||
val text = "\\include \\input{}"
|
||||
val root = LatexParser.parse(text)
|
||||
assertEquals(0, LatexInclude.analyze(root).size)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue