Skip to content

tree-sitter-textobjects functionality limited in markdown embedded code fences #90

Closed

Description

@mrjones2014

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.lua provided 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:

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:

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

  1. Save the repro.lua below and create a test markdown file with some code fences in it like:
# heading

```lua
local function foo()
  return 1
end
```
  1. nvim -u repro.lua test.md and wait for the markdown, markdown_inline, and lua parsers to install (may need a restart after first install).
  2. Place the cursor on return 1 inside the code fence.
  3. Press vif (or yif).
  4. Nothing is selected because if is not mapped. :omap if reports "No mapping found", while :omap ik shows the buffer-local @block.inner mapping, confirming textobjects are active for the buffer.
  5. 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

  1. linked a pull request that will close this issuechore(main): release 3.1.2 #91on Oct 13, 2026
  2. mehalter commented on Oct 14, 2026

    @mehalter
    Member

    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

  3. mrjones2014 commented on Oct 14, 2026

    @mrjones2014
    Author

    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.

  4. mehalter commented on Oct 14, 2026

    @mehalter
    Member

    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.nvim plugin (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 just vim.lsp.config("*", ...) will be overridden by any language server definition that provides their own before_init function which does happen pretty regularly in nvim-lspconfig specs. This method basically uses the AstroLSP setup handler to hot patch any pre-existing before_init function (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.

  5. mrjones2014 commented on Oct 14, 2026

    @mrjones2014
    Author

    Oh neat, even just syntax-wise I like that your first link feels more declarative than what I'm doing!

  6. mrjones2014 commented on Oct 14, 2026

    @mrjones2014
    Author

    Tested and this seems to work great for me, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions