ejs.nvim
Neovim Plugin for EJS Template Support
ejs.nvim is a free, open source Neovim plugin that brings first-class EJS (Embedded JavaScript) template support to Neovim. Rather than reinventing a parser, it wires up the existing tree-sitter-embedded-template grammar with language injection, LSP configuration, and LuaSnip snippets.
All dependencies beyond Neovim itself are optional. The plugin degrades gracefully when nvim-treesitter, language servers, or LuaSnip are absent, and requires zero manual configuration beyond installation.
Features
-
Tree-sitter syntax highlighting
HTML outside
<% %>tags, JavaScript inside them, via Neovim's native Tree-sitter highlighter. -
Language injection
Registers
embedded_templateas the parser for theejsfiletype, and shipsqueries/embedded_template/injections.scmto injecthtmlinto content nodes andjavascriptinto directive/output-directive code nodes. -
LSP attachment
html-lspattaches for the HTML portions andts_lsattaches for the JavaScript portions, both onFileType ejs, with duplicate-attachment guards. -
LuaSnip snippets
Snippets for common EJS patterns, registered for the
ejsfiletype, loaded only if LuaSnip is installed. -
Health checks
:checkhealth ejsverifies your Neovim version, installed Tree-sitter parsers, and language server availability.
Prerequisites
All dependencies are optional. The plugin degrades gracefully when any of them are absent.
| Dependency | Purpose | Required |
|---|---|---|
| Neovim >= 0.10 | vim.fs.root() API | Yes |
nvim-treesitter/nvim-treesitter | Parser management; required for CSS highlighting in <style> blocks | Recommended |
html-lsp (vscode-html-language-server) | HTML language server | Optional |
typescript-language-server | JavaScript/TypeScript language server | Optional |
L3MON4D3/LuaSnip | Snippet engine | Optional |
Installation
lazy.nvim
{
"connorontheweb/ejs.nvim",
ft = "ejs",
dependencies = {
"nvim-treesitter/nvim-treesitter", -- optional, recommended
"neovim/nvim-lspconfig", -- optional
"L3MON4D3/LuaSnip", -- optional
},
opts = {},
}
With opts = {}, lazy.nvim calls setup() for you.
vim-plug
Plug 'connorontheweb/ejs.nvim'
lua require('ejs').setup()
packer.nvim
use 'connorontheweb/ejs.nvim'
mini.deps
MiniDeps.add('connorontheweb/ejs.nvim')
require('ejs').setup()
No plugin manager (built-in packages)
mkdir -p ~/.local/share/nvim/site/pack/plugins/start
git clone https://github.com/connorontheweb/ejs.nvim \
~/.local/share/nvim/site/pack/plugins/start/ejs.nvim
Then add require('ejs').setup() to your init.lua (or lua require('ejs').setup() in init.vim).
Post-install: install the Tree-sitter parsers
With nvim-treesitter installed, run inside Neovim:
:TSInstall embedded_template html css
embedded_template parses the EJS structure, html handles content between EJS tags, and css handles CSS inside <style> blocks injected via the HTML parser's own injection queries.
Configuration
All options default to true. Pass overrides to require('ejs').setup(), or via opts if using lazy.nvim:
require('ejs').setup({
treesitter = true, -- register the embedded_template parser for .ejs files
lsp = true, -- attach html-lsp and ts_ls on FileType ejs
snippets = true, -- load LuaSnip snippets (silently skipped if LuaSnip absent)
})
Health Checks
Run :checkhealth ejs to diagnose your setup:
| Check | Pass condition | Failure level |
|---|---|---|
| Neovim version | >= 0.10 | Error |
embedded_template parser | Installed via :TSInstall | Error |
html parser | Installed via :TSInstall | Warning |
css parser | Installed via :TSInstall | Warning |
vscode-html-language-server | Found on $PATH | Warning |
typescript-language-server | Found on $PATH | Warning |
| LuaSnip | Installed and loadable | Warning |
How It Works
queries/embedded_template/injections.scm injects html into (content) nodes (text outside <% %> tags) and javascript into (code) nodes that are children of (directive) or (output_directive) nodes.
The HTML injection uses #set! injection.combined so all (content) fragments are merged into a single virtual HTML document before parsing. This is required for correctness: without it, each fragment is parsed independently, and fragments that start mid-document (after a </script> tag, for example) cause the HTML parser to immediately enter error recovery and produce only ERROR nodes. With injection.combined the HTML parser sees a coherent document, produces proper element nodes including style_element, and its own injection queries fire correctly to inject CSS inside <style> blocks. JavaScript injections do not use injection.combined: each (code) block is parsed as an independent JS fragment, since concatenating disconnected scriptlet blocks rarely produces valid JavaScript.
On the LSP side, ts_ls is attached with an on_attach callback that disables the documentHighlightProvider capability for the client. Without this, Neovim sends textDocument/documentHighlight to ts_ls whenever the cursor rests on an HTML node, which the server cannot handle and returns a -32603 error. A buffer-local CursorHold autocommand replaces the default dispatch: it uses vim.treesitter.get_node() to check whether the cursor is inside a (code) node and only sends the request to ts_ls when it is, clearing stale highlights when the cursor moves back into HTML content.