Repository navigation
tree-sitter-textobjects functionality limited in markdown embedded code fences #90
Description
Checklist
- I have searched through the AstroNvim documentation
- I have searched through the existing issues of this project
- I have searched the existing issues of plugins related to this issue
- I can replicate the bug with the minimal
repro.luaprovided below
Neovim version (nvim -v)
0.12.5
Operating system/version
macOS Tahoe 26.7.1
Terminal/GUI
Ghostty
Describe the bug
Treesitter textobjects that target injected languages (e.g. if/af for @function.inner/@function.outer) are not available inside injected regions such as fenced code blocks in markdown.
astrocore.treesitter applies the default textobject mappings buffer-locally
and only when the buffer's top-level language has the capture in its
textobjects query:
astrocore/lua/astrocore/treesitter.lua
Lines 295 to 313 in 4f42234
| if config.textobjects and pcall(require, "nvim-treesitter-textobjects") then | |
| for type, methods in pairs(config.textobjects) do | |
| local mode = M.textobject_modes[type] | |
| for method, keys in pairs(methods) do | |
| for key, opts in pairs(keys) do | |
| local group = opts.group or "textobjects" | |
| if M.has_capture(lang, group, string.sub(opts.query, 2)) then | |
| local callback = function() require("nvim-treesitter-textobjects." .. type)[method](opts.query, group) end | |
| for _, map_mode in ipairs(mode) do | |
| vim.keymap.set(map_mode, key, callback, { [buffer_key] = bufnr, desc = opts.desc, silent = true }) | |
| textobject_mappings[bufnr] = textobject_mappings[bufnr] or {} | |
| textobject_mappings[bufnr][map_mode] = textobject_mappings[bufnr][map_mode] or {} | |
| textobject_mappings[bufnr][map_mode][normalize_key(key)] = callback | |
| end | |
| end | |
| end | |
| end | |
| end | |
| end |
Here lang is derived from the buffer's filetype (markdown), not the language
at the cursor:
astrocore/lua/astrocore/treesitter.lua
Lines 256 to 257 in 4f42234
| local ft = vim.bo[bufnr].filetype | |
| local lang = vim.treesitter.language.get_lang(ft) |
Markdown's textobjects.scm in nvim-treesitter-textobjects only defines @class.* and @block.* captures, so if/af (and a?/i?, ao/io, aa/ia) are never mapped in markdown buffers.
However, nvim-treesitter-textobjects' select implementation already resolves injected trees: a plain global mapping to require("nvim-treesitter-textobjects.select").select_textobject("@function.inner", "textobjects") works correctly inside a lua/rust/python code fence in a markdown buffer. The per-buffer has_capture gate on the top-level language makes these textobjects unusable exactly where injection support would kick in.
Steps to Reproduce
- Save the
repro.luabelow and create a test markdown file with some code fences in it like:
# heading
```lua
local function foo()
return 1
end
```nvim -u repro.lua test.mdand wait for themarkdown,markdown_inline, andluaparsers to install (may need a restart after first install).- Place the cursor on
return 1inside the code fence. - Press
vif(oryif). - Nothing is selected because
ifis not mapped.:omap ifreports "No mapping found", while:omap ikshows the buffer-local@block.innermapping, confirming textobjects are active for the buffer. - For contrast, add a global mapping and repeat step 4; it selects the function body inside the fence:
vim.keymap.set({ "x", "o" }, "if", function()
require("nvim-treesitter-textobjects.select").select_textobject("@function.inner", "textobjects")
end)Expected behavior
vif/yif (and the other default textobjects) work inside injected language regions, like fenced code blocks in markdown, since the underlying plugin supports injections. For example, the has_capture check could consider the language of the injected tree at the cursor at mapping-invocation time (map the key, let the callback no-op when no capture matches), rather than deciding at buffer setup time based only on the top-level language.
Screenshots
No response
Additional Context
No response
Minimal configuration
-- save as repro.lua
-- run with nvim -u repro.lua
-- DO NOT change the paths
local root = vim.fn.fnamemodify("./.repro", ":p")
-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "runtime", "cache" }) do
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end
-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
-- stylua: ignore
vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", "--branch=stable", lazypath })
end
vim.opt.rtp:prepend(vim.env.LAZY or lazypath)
-- install plugins
local plugins = {
{ "AstroNvim/AstroNvim", import = "astronvim.plugins" },
{ "AstroNvim/astrocommunity" },
-- add any other plugins/customizations here
}
require("lazy").setup(plugins, {
root = root .. "/plugins",
})
-- add anything else here (autocommands, vim.filetype, etc.)
Activity
mehalter commentedon Oct 14, 2026
Thanks for reporting! This is a great finding that also wasn't initially supported in the tree-sitter-textobjects implementation back when it used to configure itself. v3.1.2 adds a method for calculating possible injected languages to help keep the available keymappings minimal to what's actually potentially available in the current file. Let me know if there are any issues! I have done some testing and it seems to work in the cases I have explored. Also did a fair amount of benchmarking and it should only add like ~1-4ms to setting up the buffer local mappings
mrjones2014 commentedon Oct 14, 2026
Thank you for fixing it! I’ll test it out when I’m at my desk.
I ran into it because I relied on it while writing README.md’s, and I only just recently switched from a giant hand rolled config which was becoming cumbersome to maintain, to AstroNvim.
mehalter commentedon Oct 14, 2026
Welcome to the fray! Very cool to see you giving it a try, I've been a long time fan of your plugins and how well they are written and work!
One thing you might be interested in is how I have set up your
codesettings.nvimplugin (https://code.mehalter.com/mehalter/nvim/src/branch/main/lua/plugins/codesettings.lua). It is a bit funky mainly because the recommended setup of using justvim.lsp.config("*", ...)will be overridden by any language server definition that provides their ownbefore_initfunction which does happen pretty regularly innvim-lspconfigspecs. This method basically uses the AstroLSP setup handler to hot patch any pre-existingbefore_initfunction (if it exists) that may be available with codesettings. This works in a very nice general way outside of cases where you may have a custom handler for other language servers where you still have to remember to manually set up codesettings there as well (I do this with the julia language server here: https://code.mehalter.com/mehalter/nvim/src/branch/main/lua/plugins/astrolsp.lua#L34-L68)Sorry this is a bit out of context with the rest of this issue, but wanted to get your feedback on here, maybe this is better as a conversation on Discord now that you know how to contact me there.
mrjones2014 commentedon Oct 14, 2026
Oh neat, even just syntax-wise I like that your first link feels more declarative than what I'm doing!
mrjones2014 commentedon Oct 14, 2026
Tested and this seems to work great for me, thanks!