Skip to content

Instantly share code, notes, and snippets.

@b0o
Last active December 3, 2023 00:56
Show Gist options
  • Save b0o/59a46ecfdb1196312da0c92273b56aa8 to your computer and use it in GitHub Desktop.
Save b0o/59a46ecfdb1196312da0c92273b56aa8 to your computer and use it in GitHub Desktop.
LSP support in injected code blocks with Otter.nvim

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.

Dependencies

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.

Usage

Otter's documentation is pretty lacking. Here's the gist of how it works:

  1. 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.
  2. Diagnostics usually won't appear until after you save the file.

  3. For hover, definition, and other LSP actions, you must use otter.ask_{action_name}() instead of vim.lsp.buf.{action_name}().

  4. Otter has an integration with nvim-cmp for completion support. Add the otter source to enable it.

Tips for Better UX

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.

Caveats

  • 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.
-- See https://gist.github.com/b0o/59a46ecfdb1196312da0c92273b56aa8
-- for more details.
local M = {}
local otter = require 'otter'
local keeper = require 'otter.keeper'
local extensions = require 'otter.tools.extensions'
-- If you want to use an injected language that's not in Otter's
-- default list, you can add it here. If it's not in the list,
-- and you don't add it here, the lsp_action wrapper won't work for
-- that language.
extensions.glsl = 'glsl'
extensions.json = 'json'
extensions.lua = 'lua'
otter.setup {
buffers = {
set_filetype = true,
},
}
-- Wrapper that checks if the current buffer has a tree-sitter parser and
-- has injected languages. If so, it activates the injected languages 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.
-- Example usage:
-- vim.api.nvim_set_keymap('n', 'K', function()
-- lsp_action('hover')
-- end)
M.lsp_action = function(action_name)
local injected = {}
local bufnr = vim.api.nvim_get_current_buf()
local parser = vim.treesitter.get_parser(bufnr)
local function do_action()
if #injected > 0 and keeper._otters_attached[bufnr] then
otter['ask_' .. action_name]()
else
vim.lsp.buf[action_name]()
end
end
if not parser then
do_action()
return
end
for _, node in pairs(parser:children()) do
local lang = node:lang()
if vim.tbl_contains(extensions, lang) then
table.insert(injected, lang)
end
end
if #injected == 0 then
do_action()
return
end
local langs = keeper._otters_attached[bufnr] and keeper._otters_attached[bufnr].languages or {}
for _, lang in ipairs(injected) do
if not vim.tbl_contains(langs, lang) then
vim.notify('Activating Otter for ' .. table.concat(injected, ', '))
otter.activate(injected)
vim.defer_fn(function()
do_action()
end, 0)
return
end
end
do_action()
end
return M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment