LSP support in injected code blocks with Otter.nvim
Otter.nvim makes it possible to attach LSPs to embedded code blocks. For example, inside JavaScript/TypeScript files, you can use tagged template literals:
const frag = glsl`
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
const bar = html`
<div>
<p>hello</p>
</div>
`;
In Markdown, you can use fenced code blocks with language identifiers.
Generally, Otter works, but it has some shortcomings. Here's how I've configured it to work for my needs.
Otter depends on treesitter and only works with parsers that support injecting other languages. You will need the treesitter parser for both the outer and inner languages installed.
Otter's documentation is pretty lacking. Here's the gist of how it works:
-
You must run
otter.activate { 'lang1', 'lang2', ... }
inside the file to activate the languages you want to use.- In the background, Otter will create a new hidden buffer for each language you activate and populate it with the contents of the respective code block(s). The LSP is attached to this hidden buffer.
-
Diagnostics usually won't appear until after you save the file.
-
For hover, definition, and other LSP actions, you must use
otter.ask_{action_name}()
instead ofvim.lsp.buf.{action_name}()
. -
Otter has an integration with nvim-cmp for completion support. Add the
otter
source to enable it.
It's a bit awkward to remember to use otter.attach
and otter.ask_{action_name}
, so I've written the lsp_action
wrapper below to try to handle this transparently.
It checks if the current buffer has a tree-sitter parser and
has any injected languages. If so, it activates the injected languages in Otter and
then runs the action via Otter. Otherwise, it runs the action via vim.lsp.buf.
If all the injected languages are already activated, it does not re-activate
them.
Here's an example of how to the wrapper for hover:
vim.api.nvim_set_keymap("n", "K", function()
lsp_action("hover")
end)
With this approach, in order to trigger otter to activate for completion/diagnostics,
I usually trigger my hover
mapping which will activate otter if it's not already
activated. Then, diagnostics and completion should work as expected.
- Otter does not work well for all languages, especially if the language depends on project-specific configuration. Rust is an example that does not work well.
- Otter has some issues with formatting for some outer languages. Markdown works well, but in JS/TS, it replaces too much code when formatting. See otter.nvim#72.
- For formatting to work, the LSP needs to support
textDocument/rangeFormatting
.