feat(solidlsp): add LaTeX language server via texlab (#1600)

Register the LATEX language (.tex/.bib/.sty/.cls) backed by texlab,
downloaded as a sha256-verified single-binary from GitHub Releases
(v5.25.1) across macOS/Linux/Windows. Document symbols expose the
sectioning hierarchy; definitions/references cover labels and citations.

Includes a smoke-test fixture repo and basic document-symbol tests.

Co-authored-by: KoscheiiB <KoscheiiB@users.noreply.github.com>
This commit is contained in:
BBPandKoscheiiB authored and GitHub committed 2026-06-25 11:21:39 +02:00
1 parent a0435467b6
commit 5d44d017c8
16 files changed
+520 -2

No files matched your search

+5
View File
@@ -39,6 +39,11 @@ Status of the `main` branch. Changes prior to the next official version change w
so they can no longer inherit and clobber Serena's stdin, which is the JSON-RPC pipe under the stdio
transport. #1577
- Improve quoting of arguments in shell executions
- Add **LaTeX** support (experimental) via [texlab](https://github.com/latex-lsp/texlab). Auto-downloads a
SHA-256-verified prebuilt texlab binary (osx-arm64/x64, linux-arm64/x64, win-x64); covers `.tex`, `.bib`,
`.sty`, and `.cls` files. Provides sectioning document symbols (including beamer frames), label/citation
definitions, and `\ref`/`\cite` references. Must be explicitly enabled via language `latex`. texlab is
GPL-3.0 and runs as a separate downloaded process.
* JetBrains:
- Add configuration option `jetbrains_launch_command`, allowing Serena to spawn IDE instances automatically
+1 -1
View File
@@ -110,7 +110,7 @@ Serena incorporates a powerful abstraction layer for the integration of language
The underlying language servers are typically open-source projects or at least freely available for use.
When using Serena's language server backend, we provide **support for over 40 programming languages**, including
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
### The Serena JetBrains Plugin
@@ -97,6 +97,10 @@ Some languages require additional installations or setup steps, as noted.
* **Julia**
* **Kotlin**
(uses the pre-alpha [official kotlin LS](https://github.com/Kotlin/kotlin-lsp), some issues may appear)
* **LaTeX**
(experimental; must be explicitly enabled via language `latex`; uses [texlab](https://github.com/latex-lsp/texlab),
auto-downloaded as a SHA-256-verified prebuilt binary; supports `.tex`, `.bib`, `.sty`, and `.cls` files; texlab is
GPL-3.0 and runs as a separate downloaded process)
* **Lean 4**
(requires `lean` and `lake` installed via [elan](https://github.com/leanprover/elan); uses the built-in Lean 4 LSP;
the project must be a Lake project with `lake build` run before use)
+1
View File
@@ -337,6 +337,7 @@ markers = [
"fsharp: language server running for F#",
"rego: language server running for Rego",
"markdown: language server running for Markdown",
"latex: language server running for LaTeX",
"julia: Julia language server tests",
"fortran: language server running for Fortran",
"haskell: Haskell language server tests",
@@ -0,0 +1,199 @@
"""
Provides LaTeX specific instantiation of the LanguageServer class using texlab.
texlab is downloaded as a prebuilt binary from the latex-lsp/texlab GitHub releases.
"""
import logging
import os
from typing import cast
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PathUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
TEXLAB_VERSION = "5.25.1"
TEXLAB_BASE_URL = f"https://github.com/latex-lsp/texlab/releases/download/v{TEXLAB_VERSION}"
TEXLAB_ALLOWED_HOSTS = (
"github.com",
"release-assets.githubusercontent.com",
"objects.githubusercontent.com",
)
TEXLAB_SHA256_BY_PLATFORM = {
"osx-arm64": "3755e9d1d4ad0b25135bdacd2fb453a612e88f48133185f96d660fa550398f66",
"osx-x64": "11289a231f0cf382857a6a4a2eda1ba9f4f4e950af343b455797e3922d13b1ea",
"linux-arm64": "e0d8e0b27b2e6e3526fa5019323bb3fddb1202a0f0049e527672b5ff323cc15e",
"linux-x64": "c8260b2fd2849cbad7d1f54c4ffa0389f34664b049392107bc4f7f9c8ec542ba",
"win-x64": "aa5fc1fe6004c17cd83086a57a8c8f28bb3f360914872711bfbb83490dc3c19e",
}
class TexlabLanguageServer(SolidLanguageServer):
"""
Provides LaTeX specific instantiation of the LanguageServer class using texlab.
Symbol navigation maps onto LaTeX structure: document symbols are the sectioning
hierarchy, definitions/references cover labels and citations.
"""
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["_minted", "_build", "build", "out", "auto"]
@classmethod
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
"""Download and install texlab for the current platform if not already present."""
platform_id = PlatformUtils.get_platform_id()
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="Texlab",
description="texlab for macOS (ARM64)",
url=f"{TEXLAB_BASE_URL}/texlab-aarch64-macos.tar.gz",
platform_id="osx-arm64",
archive_type="gztar",
binary_name="texlab",
sha256=TEXLAB_SHA256_BY_PLATFORM["osx-arm64"],
allowed_hosts=TEXLAB_ALLOWED_HOSTS,
),
RuntimeDependency(
id="Texlab",
description="texlab for macOS (x64)",
url=f"{TEXLAB_BASE_URL}/texlab-x86_64-macos.tar.gz",
platform_id="osx-x64",
archive_type="gztar",
binary_name="texlab",
sha256=TEXLAB_SHA256_BY_PLATFORM["osx-x64"],
allowed_hosts=TEXLAB_ALLOWED_HOSTS,
),
RuntimeDependency(
id="Texlab",
description="texlab for Linux (ARM64)",
url=f"{TEXLAB_BASE_URL}/texlab-aarch64-linux.tar.gz",
platform_id="linux-arm64",
archive_type="gztar",
binary_name="texlab",
sha256=TEXLAB_SHA256_BY_PLATFORM["linux-arm64"],
allowed_hosts=TEXLAB_ALLOWED_HOSTS,
),
RuntimeDependency(
id="Texlab",
description="texlab for Linux (x64)",
url=f"{TEXLAB_BASE_URL}/texlab-x86_64-linux.tar.gz",
platform_id="linux-x64",
archive_type="gztar",
binary_name="texlab",
sha256=TEXLAB_SHA256_BY_PLATFORM["linux-x64"],
allowed_hosts=TEXLAB_ALLOWED_HOSTS,
),
RuntimeDependency(
id="Texlab",
description="texlab for Windows (x64)",
url=f"{TEXLAB_BASE_URL}/texlab-x86_64-windows.zip",
platform_id="win-x64",
archive_type="zip",
binary_name="texlab.exe",
sha256=TEXLAB_SHA256_BY_PLATFORM["win-x64"],
allowed_hosts=TEXLAB_ALLOWED_HOSTS,
),
]
)
dependency = deps.get_single_dep_for_current_platform()
install_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), f"texlab-{TEXLAB_VERSION}")
texlab_executable_path = deps.binary_path(install_dir)
if not os.path.exists(texlab_executable_path):
log.info(f"Downloading texlab from {dependency.url}")
deps.install(install_dir)
assert os.path.exists(texlab_executable_path), f"texlab executable not found at {texlab_executable_path}"
if platform_id.value != "win-x64":
os.chmod(texlab_executable_path, 0o755)
return texlab_executable_path
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a TexlabLanguageServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
texlab_executable_path = self._setup_runtime_dependencies(solidlsp_settings)
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=texlab_executable_path, cwd=repository_root_path),
"latex",
solidlsp_settings,
)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""Returns the initialize params for the texlab Language Server."""
root_uri = PathUtils.path_to_uri(repository_absolute_path)
result = {
"processId": os.getpid(),
"locale": "en",
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"rename": {"dynamicRegistration": True},
},
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
},
"workspaceFolders": [
{
"name": os.path.basename(repository_absolute_path),
"uri": root_uri,
}
],
}
return cast(InitializeParams, result)
def _start_server(self) -> None:
"""Start the texlab server process."""
def register_capability_handler(params: dict) -> None:
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params: dict) -> None:
return
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
log.info("Starting texlab server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
log.info("Sending initialize request from LSP client to texlab and awaiting response")
init_response = self.server.send.initialize(initialize_params)
assert "textDocumentSync" in init_response["capabilities"]
assert "documentSymbolProvider" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]
self.server.notify.initialized({})
+13 -1
View File
@@ -159,6 +159,11 @@ class Language(str, Enum):
Must be explicitly specified as the main language, not auto-detected.
This is an edge case primarily useful when working on documentation-heavy projects.
"""
LATEX = "latex"
"""texlab language server for LaTeX/BibTeX (experimental).
Must be explicitly specified as the main language, not auto-detected.
Provides sectioning-hierarchy document symbols plus label/citation definitions and references.
"""
YAML = "yaml"
"""YAML language server (experimental).
Must be explicitly specified as the main language, not auto-detected.
@@ -248,6 +253,7 @@ class Language(str, Enum):
self.RUBY_SOLARGRAPH,
self.PHP_PHPACTOR,
self.MARKDOWN,
self.LATEX,
self.YAML,
self.JSON,
self.TOML,
@@ -263,7 +269,7 @@ class Language(str, Enum):
"""Whether the supported language should be considered a programming language.
Solidlsp supports languages like markdown or json, this method returns False for them.
"""
return self not in frozenset((self.MARKDOWN, self.JSON, self.TOML, self.YAML, self.ANSIBLE))
return self not in frozenset((self.MARKDOWN, self.LATEX, self.JSON, self.TOML, self.YAML, self.ANSIBLE))
def __str__(self) -> str:
return self.value
@@ -429,6 +435,8 @@ class Language(str, Enum):
return FilenameMatcher(".rego")
case self.MARKDOWN:
return FilenameMatcher(".md", ".markdown")
case self.LATEX:
return FilenameMatcher(".tex", ".bib", ".sty", ".cls")
case self.SCALA:
return FilenameMatcher(".scala", ".sbt")
case self.JULIA:
@@ -681,6 +689,10 @@ class Language(str, Enum):
from solidlsp.language_servers.marksman import Marksman
return Marksman
case self.LATEX:
from solidlsp.language_servers.texlab_language_server import TexlabLanguageServer
return TexlabLanguageServer
case self.R:
from solidlsp.language_servers.r_language_server import RLanguageServer
+1
View File
@@ -256,6 +256,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = {
Language.JAVA: [pytest.mark.java],
Language.KOTLIN: [pytest.mark.kotlin, pytest.mark.skipif(is_ci, reason="Kotlin LSP JVM crashes on restart in CI")],
Language.LEAN4: [pytest.mark.lean4, pytest.mark.skipif(_sh.which("lean") is None, reason="Lean is not installed")],
Language.LATEX: [pytest.mark.latex],
Language.MSL: [pytest.mark.msl],
Language.PHP: [pytest.mark.php],
Language.PHP_PHPACTOR: [pytest.mark.php],
@@ -0,0 +1,7 @@
*.aux
*.log
*.out
*.pdf
*.toc
*.synctex.gz
build/
@@ -0,0 +1,35 @@
\documentclass{article}
\usepackage{amsmath}
\title{Serena Texlab Smoke Test}
\author{Test Author}
\begin{document}
\maketitle
\section{Introduction}
\label{sec:intro}
This is the introduction. It points forward to Section~\ref{sec:methods}.
\section{Methods}
\label{sec:methods}
The methods build on Section~\ref{sec:intro}.
\subsection{Implementation Details}
\label{sec:details}
Details that refer back to Section~\ref{sec:methods} once more.
\section{Conclusion}
\label{sec:conclusion}
Wrapping up, see Section~\ref{sec:details}.
\input{sections/background.tex}
For the broader context behind these methods, see Section~\ref{sec:background}.
The typesetting system itself is described by Lamport~\cite{lamport1994} and
Knuth~\cite{knuth1984}.
\bibliographystyle{plain}
\bibliography{references}
\end{document}
@@ -0,0 +1,13 @@
@book{knuth1984,
author = {Knuth, Donald E.},
title = {The {TeXbook}},
publisher = {Addison-Wesley},
year = {1984},
}
@book{lamport1994,
author = {Lamport, Leslie},
title = {{LaTeX}: A Document Preparation System},
publisher = {Addison-Wesley},
year = {1994},
}
@@ -0,0 +1,9 @@
\section{Background}
\label{sec:background}
The document model used here follows the sectioning structure of the TeX
typesetting system~\cite{knuth1984}. Cross-referencing between sections relies on
stable label keys, which is the behaviour this fixture exercises.
\subsection{Prior Work}
\label{sec:prior-work}
A fuller description of the macro layer appears in the LaTeX manual~\cite{lamport1994}.
@@ -0,0 +1,33 @@
\documentclass{beamer}
\usetheme{default}
\title{Serena Texlab Beamer Smoke Test}
\author{Test Author}
\begin{document}
\frame{\titlepage}
\section{Overview}
\begin{frame}
\frametitle{Introduction}
\label{frame:intro}
An introductory frame for the overview section.
\end{frame}
\begin{frame}
\frametitle{Methodology}
\label{frame:method}
A frame describing the methodology, see Section~\ref{sec:results}.
\end{frame}
\section{Results}
\label{sec:results}
\begin{frame}
\frametitle{Findings}
A closing frame summarising the findings.
\end{frame}
\end{document}
+11
View File
@@ -517,6 +517,9 @@ FIND_SYMBOL_REFERENCES_CASES = [
FindSymbolCase(
language=Language.RUST, id="rust_add_function", symbol_name="add", expected_kind="Function", expected_file="lib.rs"
).to_pytest_param(),
FindSymbolCase(
language=Language.LATEX, id="latex_methods_section", symbol_name="Methods", expected_kind="Module", expected_file="main.tex"
).to_pytest_param(),
]
FIND_REFERENCE_CASES = [
@@ -597,6 +600,13 @@ FIND_REFERENCE_CASES = [
).to_pytest_param(
pytest.mark.xfail(reason="F# language server is unreliable"), # See issue #1040
),
FindReferenceCase(
language=Language.LATEX,
id="latex_background_refs",
symbol_name="Background",
definition_file="sections/background.tex",
reference_file="main.tex",
).to_pytest_param(),
]
FIND_DEFINING_SYMBOL_REGEX_ERROR_CASES = [
@@ -779,6 +789,7 @@ def serena_config():
Language.HAXE,
Language.LEAN4,
Language.MSL,
Language.LATEX,
]:
repo_path = get_repo_path(language)
if repo_path.exists():
+37
View File
@@ -0,0 +1,37 @@
"""Basic tests for the texlab-based LaTeX language server."""
import pytest
from serena.symbol import LanguageServerSymbol
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
@pytest.mark.latex
class TestLatexLanguageServerBasics:
"""Basic functionality of the LaTeX (texlab) language server."""
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_document_symbols_are_sections(self, language_server: SolidLanguageServer) -> None:
"""Sectioning commands should surface as document symbols."""
symbols, _roots = language_server.request_document_symbols("main.tex").get_all_symbols_and_roots()
assert len(symbols) > 0, "Should find at least some symbols in main.tex"
names = {s.get("name", "") for s in symbols}
for expected in ("Introduction", "Methods", "Conclusion"):
assert expected in names, f"Expected section '{expected}' among document symbols, got: {sorted(names)}"
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_subsection_symbol_present(self, language_server: SolidLanguageServer) -> None:
"""A nested subsection should also be exposed as a symbol."""
symbols, _roots = language_server.request_document_symbols("main.tex").get_all_symbols_and_roots()
names = {s.get("name", "") for s in symbols}
assert "Implementation Details" in names, f"Expected the subsection symbol, got: {sorted(names)}"
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_subsection_name_path_nests_under_section(self, language_server: SolidLanguageServer) -> None:
"""A subsection's name path nests under its parent section ("section/subsection")."""
_symbols, roots = language_server.request_document_symbols("main.tex").get_all_symbols_and_roots()
matches = [m for root in roots for m in LanguageServerSymbol(root).find("Methods/Implementation Details")]
assert len(matches) == 1, matches
assert matches[0].get_name_path() == "Methods/Implementation Details"
+59
View File
@@ -0,0 +1,59 @@
r"""Beamer presentation support for the LaTeX (texlab) language server.
Beamer frames surface as ``Frame: <title>`` document symbols nested under their
section, and ``\ref`` resolution works inside frames.
"""
import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import Location
from test.solidlsp.conftest import read_repo_file
SLIDES = "slides.tex"
def _rel(location: Location) -> str:
relative_path = location["relativePath"]
assert relative_path is not None, location
return relative_path.replace("\\", "/")
@pytest.mark.latex
class TestLatexBeamer:
"""texlab handling of a beamer presentation (sections and frames)."""
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_beamer_sections_and_frames_are_symbols(self, language_server: SolidLanguageServer) -> None:
r"""Beamer sections and ``\frametitle`` frames both surface as document symbols."""
symbols, _roots = language_server.request_document_symbols(SLIDES).get_all_symbols_and_roots()
names = {s.get("name", "") for s in symbols}
for expected in ("Overview", "Results", "Frame: Introduction", "Frame: Methodology", "Frame: Findings"):
assert expected in names, f"Expected beamer symbol {expected!r}, got: {sorted(names)}"
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_beamer_frames_nest_under_sections(self, language_server: SolidLanguageServer) -> None:
r"""Frames are children of the section they appear in."""
_symbols, roots = language_server.request_document_symbols(SLIDES).get_all_symbols_and_roots()
roots_by_name = {root.get("name"): root for root in roots}
overview = roots_by_name.get("Overview")
assert overview is not None, [root.get("name") for root in roots]
overview_children = {child.get("name") for child in overview.get("children", [])}
assert overview_children == {"Frame: Introduction", "Frame: Methodology"}, overview_children
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_beamer_frame_ref_resolves_to_section(self, language_server: SolidLanguageServer) -> None:
r"""A ``\ref`` inside a beamer frame resolves to the section it targets."""
content = read_repo_file(language_server, SLIDES)
ref = find_text_coordinates(content, r"see Section~\\ref\{(sec:results)\}", require_unique=True)
section = find_text_coordinates(content, r"\\section\{(Results)\}", require_unique=True)
assert ref is not None and section is not None
definitions = language_server.request_definition(SLIDES, ref.line, ref.col)
assert len(definitions) == 1, definitions
assert _rel(definitions[0]) == "slides.tex"
assert definitions[0]["range"]["start"]["line"] == section.line
@@ -0,0 +1,92 @@
r"""Reference and definition resolution for the LaTeX (texlab) language server.
Exercises within-file and cross-file ``\ref`` -> ``\label`` resolution and
``\cite`` -> BibTeX entry resolution over the latex test repository.
"""
import os
import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import Location
from test.solidlsp.conftest import read_repo_file
MAIN = "main.tex"
BACKGROUND = os.path.join("sections", "background.tex")
BIB = "references.bib"
def _coords(language_server: SolidLanguageServer, relative_path: str, regex: str):
coords = find_text_coordinates(read_repo_file(language_server, relative_path), regex, require_unique=True)
assert coords is not None, f"pattern {regex!r} not found in {relative_path}"
return coords
def _rel(location: Location) -> str:
relative_path = location["relativePath"]
assert relative_path is not None, location
return relative_path.replace("\\", "/")
@pytest.mark.latex
class TestLatexReferences:
"""texlab reference/definition resolution within and across files."""
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_within_file_ref_resolves_to_section(self, language_server: SolidLanguageServer) -> None:
r"""A ``\ref`` resolves to the sectioning command labelled in the same file."""
ref = _coords(language_server, MAIN, r"forward to Section~\\ref\{(sec:methods)\}")
section = _coords(language_server, MAIN, r"\\section\{(Methods)\}")
definitions = language_server.request_definition(MAIN, ref.line, ref.col)
assert len(definitions) == 1, definitions
assert _rel(definitions[0]) == "main.tex"
assert definitions[0]["range"]["start"]["line"] == section.line
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_within_file_references_list_all_uses(self, language_server: SolidLanguageServer) -> None:
r"""Requesting references on a within-file label returns both ``\ref`` uses."""
label = _coords(language_server, MAIN, r"\\label\{(sec:methods)\}")
references = language_server.request_references(MAIN, label.line, label.col)
assert {_rel(ref) for ref in references} == {"main.tex"}
assert len(references) == 2, references
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_cross_file_ref_resolves_across_files(self, language_server: SolidLanguageServer) -> None:
r"""A ``\ref`` in main.tex resolves to a ``\label`` defined in another file."""
ref = _coords(language_server, MAIN, r"see Section~\\ref\{(sec:background)\}")
section = _coords(language_server, BACKGROUND, r"\\section\{(Background)\}")
definitions = language_server.request_definition(MAIN, ref.line, ref.col)
assert len(definitions) == 1, definitions
assert _rel(definitions[0]) == "sections/background.tex"
assert definitions[0]["range"]["start"]["line"] == section.line
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_cross_file_references_point_back_to_main(self, language_server: SolidLanguageServer) -> None:
r"""References on a cross-file label include the ``\ref`` site in main.tex."""
label = _coords(language_server, BACKGROUND, r"\\label\{(sec:background)\}")
ref_in_main = _coords(language_server, MAIN, r"see Section~\\ref\{(sec:background)\}")
references = language_server.request_references(BACKGROUND, label.line, label.col)
assert any(_rel(ref) == "main.tex" and ref["range"]["start"]["line"] == ref_in_main.line for ref in references), references
@pytest.mark.parametrize("language_server", [Language.LATEX], indirect=True)
def test_citation_resolves_to_bib_entry(self, language_server: SolidLanguageServer) -> None:
r"""A ``\cite`` resolves to its entry in the BibTeX file."""
cite = _coords(language_server, MAIN, r"Knuth~\\cite\{(knuth1984)\}")
entry = _coords(language_server, BIB, r"@book\{(knuth1984)")
definitions = language_server.request_definition(MAIN, cite.line, cite.col)
assert len(definitions) == 1, definitions
assert _rel(definitions[0]) == "references.bib"
assert definitions[0]["range"]["start"]["line"] == entry.line