mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-03 07:13:33 +00:00
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:
1 parent
a0435467b6
commit
5d44d017c8
16 files changed
+520
-2
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({})
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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():
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in new issue
Block a user