ejs.nvim icon

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_template as the parser for the ejs filetype, and ships queries/embedded_template/injections.scm to inject html into content nodes and javascript into directive/output-directive code nodes.

  • LSP attachment

    html-lsp attaches for the HTML portions and ts_ls attaches for the JavaScript portions, both on FileType ejs, with duplicate-attachment guards.

  • LuaSnip snippets

    Snippets for common EJS patterns, registered for the ejs filetype, loaded only if LuaSnip is installed.

  • Health checks

    :checkhealth ejs verifies 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.10vim.fs.root() APIYes
nvim-treesitter/nvim-treesitterParser management; required for CSS highlighting in <style> blocksRecommended
html-lsp (vscode-html-language-server)HTML language serverOptional
typescript-language-serverJavaScript/TypeScript language serverOptional
L3MON4D3/LuaSnipSnippet engineOptional

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:

CheckPass conditionFailure level
Neovim version>= 0.10Error
embedded_template parserInstalled via :TSInstallError
html parserInstalled via :TSInstallWarning
css parserInstalled via :TSInstallWarning
vscode-html-language-serverFound on $PATHWarning
typescript-language-serverFound on $PATHWarning
LuaSnipInstalled and loadableWarning

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.

Related