argiope.nvim
Argiope gives JavaScript tagged template literals first-class embedded-language highlighting and indentation in Neovim.
Most code highlighting assigns different hues to different parts of the syntax. However, when authoring webpages, we often write multiple languages in one file (js, html, css, markdown, etc).
Argiope takes a unique approach of giving each language its own primary hue, making it easy to distinguish each language at a glance. In this snippet, you can see that javascript is golden, html is cyan, css is green, and markdown is pink.
import { html, css, classify } from 'zilk'
const { WRAP, CARD, TITLE, LIST } = classify('MyCard')
export default ({ title, list }) => html`
<section=${WRAP}>
<article class=${CARD}>
<h2 class=${TITLE}>
${title}
</h2>
<ul class=${LIST}>
${list.map(item => html`
<li class=${LIST.ITEM}>
${item}
</li>
`)}
</ul>
</article>
</section>
`
export const styles = () => css`
.${WRAP} {
position: relative;
width: 100%;
}
.${CARD} {
background-color: var(--surface);
padding: 2rem;
}
.${TITLE} {
color: var(--ink);
font-weight: bold;
}
.${LIST.ITEM} {
margin-left: 2rem;
}
`
export const docs = ({ title, list }) => md`
## ${title}
This is some sample markdown to semantically match the html view
${list.map(item => md`
- ${item}
`)}
`
Configuration
Calling setup() is optional when using the defaults. The complete default configuration is:
require("argiope").setup({
enabled = true,
filetypes = { javascript = true },
tags = {
css = "css",
html = "html",
md = "markdown",
["raw.js"] = "javascript",
},
indent = {
enabled = true,
shiftwidth = 2,
expandtab = true,
},
highlight = { enabled = true, },
join = { enabled = true, },
palettes = {
css = "green",
html = "cyan",
javascript = "gold2",
javascript_embedded = "gray",
markdown = "violet",
},
})
Available palette names are gold (also available as beige), gold2, gray, blue, indigo, violet, blush, pink, green, and cyan.
Colorscheme
Argiope's editing support works with the user's existing colorscheme. To use the bundled theme:
vim.cmd.colorscheme("argiope")
The editor palette is adapted from the MIT-licensed Dracula palette, with a darker background and additional UI colors. Embedded HTML, CSS, Markdown, and JavaScript use separately configurable monochrome palettes. Embedded JavaScript is gray by default, independently of top-level JavaScript. See THIRD_PARTY_NOTICES.md for attribution.
The default monochrome mode also gives JavaScript its configured monochrome palette. Toggle to hybrid mode to keep the embedded languages monochrome while restoring multicolored, near-default Dracula JavaScript syntax:
vim.keymap.set("n", "<leader>zt", require("argiope").toggle_theme, {
desc = "Toggle Argiope JavaScript colors",
})
Argiope isolates the structural HTML parser from <script> and <style> child injections. A normalized highlighting pass colors literal script and style content without letting their language trees recurse through ${...} gaps.
Unknown tagged templates receive neutral highlighting under the bundled theme; ordinary untagged template strings keep normal JavaScript highlighting.
Comments
Neovim's built-in gc operator follows the registered template language: HTML and Markdown use <!-- -->, CSS uses /* */, and embedded JavaScript uses //.
For markup that should stay syntactically valid in any JavaScript template, toggle_interpolation_selection() wraps selected text in an empty interpolation:
vim.keymap.set("x", "<leader>zc", function()
require("argiope").toggle_interpolation_selection(0)
end, {
desc = "Toggle template interpolation comment",
})
Characterwise selections wrap the exact selected text and may occupy part of one line or span multiple lines. Visual Line selections wrap each nonblank line. For example, selecting margin: 0; produces ${''/* margin: 0; */}. Selecting exactly the inside or outside of ${value} produces ${''/* value */} without nesting an interpolation.
Indentation
Use Neovim's normal = operator:
gg=G reindent the whole buffer
=ip reindent the current paragraph
Argiope:
- indents template content one
shiftwidthfrom the tag line; - delegates embedded HTML and CSS structure to nvim-treesitter's indent queries, then rebases the result onto the surrounding JavaScript;
- indents multiline substitution bodies one additional
shiftwidth; - aligns closing backticks with their tag line; and
- gives unregistered tags such as
txta flat template baseline while preserving the existing JavaScript indent expression outside tagged templates.
Without the nvim-treesitter indent engine, embedded content falls back to a flat template baseline.
Joining HTML
Inside a registered HTML template, normal or Visual mode J removes indentation without adding whitespace between an opening tag and its content or between content and a closing tag:
const label = html`
<div class=${BTN.LABEL}>
${text}
</div>
`
const label = html`
<div class=${BTN.LABEL}>${text}</div>
`
Joining ordinary prose still inserts a space, and J keeps its native behavior outside HTML templates. Argiope installs this buffer-local mapping only when no other J mapping is active. Set join.enabled = false to disable it.