composite-highlighting.nvim enhances Neovim's Treesitter capabilities by enabling dynamic syntax highlighting for two different languages simultaneously within the same file.
For example, a file named my_page.html.tmpl can be highlighted as a Go Template (gotmpl) on the outside, while the content within the template is dynamically highlighted as HTML. Similarly, my_script.js.tmpl would inject JavaScript.
- Dynamic Language Injection: Injects syntax highlighting for an inner language based on the template filename (e.g.,
*.<inner_ext>.tmpl). - Configurable Template Parsers: Define which outer template languages, file extensions, and specific Treesitter nodes should be used for dynamic injection.
- Treesitter Powered: Leverages Neovim's Treesitter for accurate and efficient parsing.
- Intelligent Defaults & Fallbacks: Provides sensible defaults for common template engines and a fallback mechanism for unsupported ones.
The following screenshot shows an index.html.tmpl file. The Go template syntax is highlighted by the gotmpl parser, while the surrounding HTML structure and content are highlighted by the html parser.
- Neovim >= 0.8 (for Treesitter Lua APIs)
nvim-treesitterplugin (as this plugin configures Treesitter injections)- Treesitter parsers installed for:
- The outer template languages you configure (e.g.,
gotmpl,eruby,jinja). - The inner languages you intend to inject (e.g.,
html,javascript,css).
- The outer template languages you configure (e.g.,
Install using your favorite plugin manager:
{
"eggplannt/composite-highlighting.nvim",
dependencies = { "nvim-treesitter/nvim-treesitter" },
config = function()
require("composite-highlighting").setup({
-- Your configuration here
languages = {
{ parser = "gotmpl", extension = "tmpl" },
-- Add other template languages if needed
-- { parser = "jinja", extension = "jinja2", injection_node = "template_data" },
},
})
end,
}use {
"eggplannt/composite-highlighting.nvim",
requires = { "nvim-treesitter/nvim-treesitter" },
config = function()
require("composite-highlighting").setup({
-- Your configuration here
languages = {
{ parser = "gotmpl", extension = "tmpl" },
-- Add other template languages if needed
},
})
end,
}Important: Ensure that the Treesitter parsers for both your template language (e.g., gotmpl) and the languages you intend to inject (e.g., html, javascript, css) are installed via nvim-treesitter. You can install them with :TSInstall gotmpl html javascript etc.
The plugin is configured by calling the setup function. The main option is languages.
require("composite-highlighting").setup({
languages = {
-- Example 1: Go Templates (uses internal default injection node, likely 'text')
{
parser = "gotmpl",
extension = "tmpl",
},
-- Example 2: Jinja2, explicitly specifying the injection node
{
parser = "jinja", -- Ensure 'jinja' parser is installed (often covers .jinja2, .j2)
extension = "jinja2",
injection_node = "template_data", -- Or 'text', 'content' depending on your Jinja parser
},
-- Example 3: ERuby
{
parser = "eruby",
extension = "erb",
-- injection_node = "text", -- Often 'text' for ERB, can be explicit
},
-- Example 4: If your template parser name is the same as its common extension
-- and you want to rely on internal defaults or the 'text' fallback for the injection node.
{
parser = "mycustomtmpl", -- Assumes '.mycustomtmpl' files
-- 'extension' will default to 'mycustomtmpl' if omitted
-- 'injection_node' will use internal map or fallback to 'text'
},
},
})The languages option is an array of tables, where each table configures a template type:
parser(string, required): The name of the installed Treesitter parser for the outer template language (e.g.,"gotmpl","jinja","eruby"). The plugin will warn and skip if this parser is not installed.extension(string, optional): The file extension that identifies these template files (e.g.,"tmpl","jinja2","erb").- If omitted,
extensiondefaults to the value ofparser. - The plugin uses this to associate files with the
parser's primary filetype.
- If omitted,
injection_node(string, optional): The specific Treesitter node within theparser's grammar that should contain the injected language.- If omitted, the plugin will:
- Check an internal map of common
parser-to-node mappings (e.g.,gotmpl->text). - If not found in the map, it will fallback to using
"text"as the node name.
- Check an internal map of common
- This option is crucial for less common template engines or when the default/fallback node is not suitable. See "Finding the Correct
injection_node" below.
- If omitted, the plugin will:
For each configured language in the languages option:
- The plugin registers the specified
extensionto be recognized as the filetype associated with theparser(e.g.,.tmplfiles becomegotmpl). - It determines the Treesitter node to target for injection. The precedence is:
- User-provided
injection_nodein the language configuration. - Node from an internal
parser_to_nodemap (e.g.,gotmpl->text). - A general fallback of
"text".
- User-provided
- It then sets up a Treesitter injection query for this filetype, targeting the determined node (e.g.,
(text @injection.content)or(template_data @injection.content)). - When such a node is encountered in a file like
filename.<inner_ext>.<outer_ext>(e.g.,my_page.html.tmpl), a custom directiveinject-<parser>!is triggered. - This directive attempts to determine the
<inner_ext>:- It strips the
.<outer_ext>(e.g.,.tmpl) from the buffer's filename. - It then tries to get the filetype of the remaining part (e.g.,
my_page.html) using:vim.filetype.match({ filename = "my_page.html" })- If that fails, it extracts the extension (e.g.,
html) and triesvim.filetype.match({ filename = "file.html" }). - As a final fallback, it consults an internal map of common extensions to filetypes (e.g.,
ts->typescript,js->javascript).
- It strips the
- The determined inner filetype is then used for syntax highlighting within the targeted injection node.
- Finding the Correct
injection_node:- If the default injection behavior isn't working for your specific template language (even if the parser is installed), you might need to specify the
injection_node. - To find the correct node:
- Open a template file of that type.
- Run
:InspectTree(provided bynvim-treesitter). This opens a split window showing the syntax tree. - Navigate your cursor in the template file to the general content area where you expect the inner language to be.
- Observe the highlighted node in the
:InspectTreewindow. This node name (e.g.,template_data,content,text_blob) is what you should use for theinjection_nodeoption.
- If you find a good
injection_nodefor a common parser not yet in our internal defaults, please consider contributing it!
- If the default injection behavior isn't working for your specific template language (even if the parser is installed), you might need to specify the
- Parser Installation: Ensure both the outer template parser (e.g.,
gotmpl) and any inner language parsers (e.g.,html,javascript,css,python) you expect to be injected are installed vianvim-treesitter(:TSInstall <parser_name>). - Filename Convention: The plugin relies on the
filename.<inner_ext>.<outer_ext>convention for dynamic injection.
Contributions, issues, and feature requests are welcome! Please feel free to open an issue or submit a pull request on GitHub.
This project is licensed under the MIT License. See the LICENSE file for details.
