jinja-lsp
Language server for Jinja templates — diagnostics, navigation, completions, hover, and Jinja-aware formatting. One Rust binary, any LSP-capable editor. Static analysis only — it never imports, renders, or executes your templates or host Python.
It runs alongside your Python and HTML language servers, owning the Jinja layer end to end and staying silent everywhere else.
Features
| Diagnostics | 21 checks — undefined variables/filters/functions/tests, unused macros/imports, duplicate & shadowed bindings, inheritance errors, wrong call args, missing templates; inline noqa suppression |
| Navigation | go-to-definition (macros, blocks, templates, imports), find references, document & call hierarchy |
| Hover | built-in docs for filters/tests/functions, macro signatures, variable scope and definition site |
| Completions | variables, attributes, filters, tests, statement keywords, template paths, imported macro names |
| Signature help | macro and filter call signatures, with the active argument highlighted |
| Symbols & lenses | document symbols, semantic tokens, folding, inlay hints, reference/override code lenses |
| Code actions | quick-fixes from the diagnostic catalog, extract-to-macro, wrap-in-block/if/for, and rename |
| Formatting | Jinja-aware formatting of the template layer — jinja-lsp format |
check CLI |
the same diagnostics as a linter — jinja-lsp check . with rich / compact / json output |
Installation
curl -fsSL https://raw.githubusercontent.com/alex-oleshkevich/jinja-lsp/master/install.sh | bash
This picks the right build for your machine, checks it against the published SHA-256, and puts the binary in ~/.local/bin. It never asks for sudo and writes nothing outside that directory. If ~/.local/bin is not on your PATH, the script says so and prints the line to add.
Two environment variables change what it does. Note that they go on bash, not on curl, because the two are separate processes and only bash runs the script:
# pin a version instead of taking the latest
curl -fsSL https://raw.githubusercontent.com/alex-oleshkevich/jinja-lsp/master/install.sh \
| JINJA_LSP_VERSION=0.2.0 bash
# install somewhere other than ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/alex-oleshkevich/jinja-lsp/master/install.sh \
| JINJA_LSP_INSTALL_DIR=~/bin bash
Intel Macs have no published build. Apple Silicon, Linux (x86_64 and ARM64), and Windows do.
If you would rather use a package manager:
uv tool install jinja-lsp
pip install jinja-lsp
yay -S jinja-lsp-plus-bin # Arch Linux
The Python packages ship the same self-contained Rust binary, so neither needs a Rust toolchain, and nothing imports Python at runtime. On the AUR the package is jinja-lsp-plus-bin. The similarly named jinja-lsp-bin belongs to an unrelated project.
You can also grab an archive from the releases page and put the binary wherever you like.
Editor setup
The server is launched as a subprocess and speaks LSP over stdio (jinja-lsp lsp). There is no TCP/socket transport.
Neovim
Using nvim-lspconfig — paste this into ~/.config/nvim/init.lua:
local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")
if not configs.jinja_lsp then
configs.jinja_lsp = {
default_config = {
cmd = { "jinja-lsp", "lsp" },
filetypes = { "jinja", "jinja.html", "htmldjango" },
root_dir = lspconfig.util.root_pattern("jinja.toml", "pyproject.toml", ".git"),
-- mirrors jinja.toml; all keys optional — overlay on top of any discovered config file
init_options = {
templates = { "templates", "..." },
extras = {},
hints = {},
lint = { select = {}, ignore = {} },
},
},
}
end
lspconfig.jinja_lsp.setup({})
Neovim 0.11+: you can also use the built-in vim.lsp.config API instead:
vim.lsp.config('jinja_lsp', {
cmd = { 'jinja-lsp', 'lsp' },
filetypes = { 'jinja', 'jinja.html', 'htmldjango' },
root_markers = { 'jinja.toml', 'pyproject.toml', '.git' },
})
vim.lsp.enable('jinja_lsp')
Helix
# ~/.config/helix/languages.toml
[language-server.jinja-lsp]
command = "jinja-lsp"
args = ["lsp"]
[[language]]
name = "jinja"
language-servers = ["jinja-lsp"]
[[language]]
name = "html"
language-servers = ["jinja-lsp"]
Zed
Install from the Zed extensions panel (Cmd+Shift+X) — search for Jinja Plus and click Install (extension id jinja-plus — jinja-lsp was already taken on Zed's marketplace). It activates automatically for Jinja and HTML templates.
To control server order alongside other language servers or pass initialization options, add to ~/.config/zed/settings.json (the language-server id is jinja2-lsp and the language is Jinja2 (HTML)):
{
"languages": { "Jinja2 (HTML)": { "language_servers": ["jinja2-lsp"] } },
"lsp": { "jinja2-lsp": { "initialization_options": { "templates": ["templates"], "extras": ["starlette"] } } }
}
Configuration
Most projects need no configuration. Template directories are found automatically by looking for templates/, <project-name>/templates/, jinja/, and j2/.
When you do want to configure something, the server walks up from the project root and takes the first of these it finds:
jinja.toml[tool.jinja]inpyproject.toml
Your editor's initializationOptions are layered on top of whichever it found. The overlay replaces only the keys it sets, so an editor setting for extras will not wipe out the templates list in your jinja.toml. Clearing a setting in your editor falls back to the file value rather than leaving the old override in place. Both files are watched, so edits apply without restarting the server.
General options
| Option | Default | |
|---|---|---|
templates |
(auto-discovered) | template root directories. "..." expands to the auto-discovered set, so you can add a directory without losing the defaults |
extensions |
["html", "jinja", "jinja2", "j2"] |
file extensions to scan |
extras |
[] |
framework packs that teach the server about globals your framework injects: flask, starlette, starlette-babel, starlette-flash |
hints |
[] |
directories of hint files describing your own context variables and macros |
custom_builtins |
[] |
directories of *.md docs for third-party filters, functions, and tests |
inline_patterns |
["render_template_string"] |
host function names whose string argument is parsed as an inline template |
lint.select |
(all) | diagnostic codes or classes to enable, such as JINJA-E1 or JINJA-W |
lint.ignore |
[] |
diagnostic codes or classes to suppress |
# jinja.toml
templates = ["templates", "..."]
extras = ["starlette"]
[lint]
ignore = ["JINJA-W106"]
To suppress a finding in one place rather than project-wide, use a comment in the template: {# noqa #} for that line, {# noqa: JINJA-W201 #} for one code, or {# noqa-file #} for the whole file.
Formatter options
These live under [format] and apply to both jinja-lsp format and formatting from your editor. The formatter only rewrites what is inside Jinja delimiters, so your HTML, YAML, or whatever else the file contains is reproduced byte for byte.
| Option | Default | |
|---|---|---|
indent_size |
4 |
spaces per indent level, ignored when use_tabs is set |
use_tabs |
false |
indent with tabs instead of spaces |
space_around_pipe |
true |
x | upper rather than x|upper |
space_around_operators |
false |
a + b rather than a+b, for symbolic operators only |
space_after_comma |
true |
truncate(20, true) rather than truncate(20,true), in filter-call arguments |
space_inside_parens |
false |
truncate( 20, true ) rather than truncate(20, true), in filter-call arguments |
space_inside_variable_delimiters |
true |
{{ x }} rather than {{x}} |
space_inside_block_delimiters |
true |
{% if x %} rather than {%if x%} |
blank_lines_after_block |
0 |
blank lines to leave after a top-level {% endblock %}, {% endfor %}, and so on |
trim_blocks |
false |
drop the first newline after a {% %} tag, matching Jinja2's runtime option of the same name |
lstrip_blocks |
false |
drop leading whitespace before a {% %} tag, likewise |
preferred_quote |
"preserve" |
"single" or "double" to normalize string literals, "preserve" to leave them alone |
newline_at_eof |
true |
end the file with exactly one newline |
trim_trailing_whitespace |
true |
strip trailing whitespace from every line |
Two things the formatter deliberately leaves alone. Keyword operators (and, or, is, in, not) always keep their surrounding spaces, whatever space_around_operators says, because removing them would change how the expression tokenizes: andb is one identifier, not and followed by b. And the comma and paren options above cover filter-call arguments only. Commas you wrote in a macro call, a dict, or a list stay exactly as you typed them, so {{ post_url(post,absolute=true) }} is left untouched.
# jinja.toml
[format]
indent_size = 2
space_around_pipe = false
preferred_quote = "double"
CLI
jinja-lsp lsp # run the language server over stdio
jinja-lsp check PATH [--select CODES] [--ignore CODES] [--format rich|compact|json]
jinja-lsp format PATH [--check]
jinja-lsp doctor [--config PATH] # report what it discovers here
doctor answers "why is it not seeing my templates". It prints the config file it
found (or that it fell back to zero-config), each template directory with how many
files matched, the builtin sources that loaded and what each contributed, and any
*.hints.md sidecars. It reports directories you configured but that do not exist,
which the indexer skips silently, and exits 1 when it finds a problem.
check's json output matches the format the test suite asserts against, so it diffs cleanly in CI. format rewrites the Jinja layer only and is round-trip safe.
Development
Every routine task has a just recipe — run just to list them.
just build
just test # cargo nextest run
just test-e2e # Python LSP-protocol suite against the real binary
just check-all # everything CI gates on
License
MIT
Metadata
Release files for jinja-lsp 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| jinja_lsp-0.4.2-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details for jinja_lsp-0.4.2-py3-none-win_amd64.whl |
| jinja_lsp-0.4.2-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details for jinja_lsp-0.4.2-py3-none-manylinux_2_28_x86_64.whl |
| jinja_lsp-0.4.2-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details for jinja_lsp-0.4.2-py3-none-manylinux_2_28_aarch64.whl |
| jinja_lsp-0.4.2-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details for jinja_lsp-0.4.2-py3-none-macosx_11_0_arm64.whl |
Total release size: 12.3 MB
Release files / jinja_lsp-0.4.2-py3-none-win_amd64.whl
| Download URL | jinja_lsp-0.4.2-py3-none-win_amd64.whl |
|---|---|
| Size | 3.1 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
59f0f1361bd979f4383111e41f3d7de5e5c5760aacf63bbfbfe1a5f22f5b98ae
|
|
BLAKE2b-256 checksum How to use checksums |
c1552fc0d7286fe2f5b091642723c7e8dee02689339f71546fb9665ef919898e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / jinja_lsp-0.4.2-py3-none-manylinux_2_28_x86_64.whl
| Download URL | jinja_lsp-0.4.2-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 3.2 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
e5e3d08d1adcedf6e98aae2f02761bf7de1914fa13c6f7132133aac6d1a5883a
|
|
BLAKE2b-256 checksum How to use checksums |
fd8dcde77b74d682977ad9a1887b120fca84fbb82b81bf3d607aa86d6189abaf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / jinja_lsp-0.4.2-py3-none-manylinux_2_28_aarch64.whl
| Download URL | jinja_lsp-0.4.2-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 3.0 MB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
f79b1bc66fa4c12574e8aa07a1610a7ea98ef2c2ad3538656cad5b81f725ccca
|
|
BLAKE2b-256 checksum How to use checksums |
dca5add79f8f2a3b2b113187281b3b2d69d4c68b1b745c5d04ba29d8499f9de5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / jinja_lsp-0.4.2-py3-none-macosx_11_0_arm64.whl
| Download URL | jinja_lsp-0.4.2-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 3.0 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
51d290217022e9d49741cb31060e76c33048bf9f5303e0887229d888af1fd28c
|
|
BLAKE2b-256 checksum How to use checksums |
bba0866e2a044a37f5ed342b729629178a14203b8ba1e8dda7df2f0b030d39e3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logSecurity for jinja-lsp 0.4.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 4 files in this release have been uploaded with provenance metadata.
Attestation for jinja_lsp-0.4.2-py3-none-win_amd64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.