nvim-treesitter-textobjects icon indicating copy to clipboard operation
nvim-treesitter-textobjects copied to clipboard

Allow/document to jump to beginning/end of functions

Open samueloph opened this issue 1 year ago • 12 comments

Is your feature request related to a problem? Please describe. One of the most useful motions for me is being able to jump to the beginning/end of a function. Unfortunately, using neovim's native ]m and ]M doesn't work for every language/file (https://neovim.io/doc/user/motion.html#%5Bm), as it doesn't uses treesitter.

Describe the solution you'd like I would like a solution that lets me jump to beginning/end of function/class using treesitter's objects. If this is already supported by the plugin, I would like it to be listed in the README examples, as I don't know how to make it behave like that. I'm looking for something that overrides ]m and ]M using treesitter's objects.

Describe alternatives you've considered The keymaps for ["af"] = "@function.outer", ["if"] = "@function.inner", are almost a perfect match of what I'm looking for, so I try to use them whenever possible. I would like something that can replace the ]m [m [M ]M motions though.

Additional context I'm a bit new to treesitter, so if this doesn't sound like a reasonable request, let me know.

samueloph avatar Nov 05 '24 10:11 samueloph

It's a reasonable idea, and I've tried to achieve it multiple times since I installed this plugin.

you can add:

goto_next_start = {
    ["]m"] = "@function.outer",
}

But it would go to a sibling start, not the parent function, like the af or if does.

It would be great to have something like: goto_parent_start and goto_parent_end jumps.

carlos-algms avatar Nov 28 '24 10:11 carlos-algms

same problem Have you solved it yet or find alternative solutions

reimu1234 avatar Jan 18 '25 01:01 reimu1234

I wrote some code to achieve the jump to the parent function start/end

It currently works for lua, js, and ts:

You should able to add a new function definition to the list, and it would work for your language.

local function_node_types = {
    "arrow_function",
    "function_declaration",
    "function_definition",
}

local function is_function_node(node)
    return vim.tbl_contains(function_node_types, node:type())
end

--- @param node TSNode|nil
--- @param types string[]
--- @return TSNode|nil
local function find_parent(node, types)
    if node == nil then
        return nil
    end

    if is_function_node(node) then
        return node
    end

    return find_parent(node:parent(), types)
end

--- @param current_node TSNode|nil
--- @param to_end? boolean
local function jump_to_node(current_node, to_end)
    if not current_node then
        return
    end

    --- @type TSNode|nil
    local function_node

    if is_function_node(current_node) then
        function_node = current_node
    else
        function_node = find_parent(
            current_node:parent(), -- getting parent to avoid being stuck in the same function
            function_node_types
        )
    end

    if not function_node then
        return
    end

    local start_row, start_col, end_row, end_col = function_node:range()
    local current_row, current_col = unpack(vim.api.nvim_win_get_cursor(0))

    local dest_row = (to_end and end_row or start_row) + 1
    local dest_col = to_end and (end_col - 1) or start_col

    if current_row == dest_row and current_col == dest_col then
        jump_to_node(function_node:parent(), to_end)
        return
    end

    vim.cmd("normal! m'") -- set jump list so I can jump back
    vim.api.nvim_win_set_cursor(0, { dest_row, dest_col })
end

vim.keymap.set("n", "[f", function()
    local current_node = vim.treesitter.get_node({ ignore_injections = false })
    jump_to_node(current_node, false)
end, {
    desc = "Jump to the start of the current function",
})

vim.keymap.set("n", "]f", function()
    local current_node = vim.treesitter.get_node({ ignore_injections = false })
    jump_to_node(current_node, true)
end, {
    desc = "Jump to the end of the current function",
})

carlos-algms avatar Jan 18 '25 08:01 carlos-algms

I wrote some code to achieve the jump to the parent function start/end

It currently works for lua, js, and ts:

You should able to add a new function definition to the list, and it would work for your language.

local function_node_types = { "arrow_function", "function_declaration", "function_definition", }

local function is_function_node(node) return vim.tbl_contains(function_node_types, node:type()) end

--- @param node TSNode|nil --- @param types string[] --- @return TSNode|nil local function find_parent(node, types) if node == nil then return nil end

if is_function_node(node) then
    return node
end

return find_parent(node:parent(), types)

end

--- @param current_node TSNode|nil --- @param to_end? boolean local function jump_to_node(current_node, to_end) if not current_node then return end

--- @type TSNode|nil
local function_node

if is_function_node(current_node) then
    function_node = current_node
else
    function_node = find_parent(
        current_node:parent(), -- getting parent to avoid being stuck in the same function
        function_node_types
    )
end

if not function_node then
    return
end

local start_row, start_col, end_row, end_col = function_node:range()
local current_row, current_col = unpack(vim.api.nvim_win_get_cursor(0))

local dest_row = (to_end and end_row or start_row) + 1
local dest_col = to_end and (end_col - 1) or start_col

if current_row == dest_row and current_col == dest_col then
    jump_to_node(function_node:parent(), to_end)
    return
end

vim.cmd("normal! m'") -- set jump list so I can jump back
vim.api.nvim_win_set_cursor(0, { dest_row, dest_col })

end

vim.keymap.set("n", "[f", function() local current_node = vim.treesitter.get_node({ ignore_injections = false }) jump_to_node(current_node, false) end, { desc = "Jump to the start of the current function", })

vim.keymap.set("n", "]f", function() local current_node = vim.treesitter.get_node({ ignore_injections = false }) jump_to_node(current_node, true) end, { desc = "Jump to the end of the current function", })

Running well, thank you so much

reimu1234 avatar Jan 18 '25 10:01 reimu1234

require'nvim-treesitter.configs'.setup {
  textobjects = {
    move = {
      enable = true,
      set_jumps = true, -- whether to set jumps in the jumplist
      goto_next_start = {
        ["]m"] = "@function.outer",
        ["]]"] = { query = "@class.outer", desc = "Next class start" },
        --
        -- You can use regex matching (i.e. lua pattern) and/or pass a list in a "query" key to group multiple queries.
        ["]o"] = "@loop.*",
        -- ["]o"] = { query = { "@loop.inner", "@loop.outer" } }
        --
        -- You can pass a query group to use query from `queries/<lang>/<query_group>.scm file in your runtime path.
        -- Below example nvim-treesitter's `locals.scm` and `folds.scm`. They also provide highlights.scm and indent.scm.
        ["]s"] = { query = "@local.scope", query_group = "locals", desc = "Next scope" },
        ["]z"] = { query = "@fold", query_group = "folds", desc = "Next fold" },
      },
      goto_next_end = {
        ["]M"] = "@function.outer",
        ["]["] = "@class.outer",
      },
      goto_previous_start = {
        ["[m"] = "@function.outer",
        ["[["] = "@class.outer",
      },
      goto_previous_end = {
        ["[M"] = "@function.outer",
        ["[]"] = "@class.outer",
      },
      -- Below will go to either the start or the end, whichever is closer.
      -- Use if you want more granular movements
      -- Make it even more gradual by adding multiple queries and regex.
      goto_next = {
        ["]d"] = "@conditional.outer",
      },
      goto_previous = {
        ["[d"] = "@conditional.outer",
      }
    },
  },
}

This is from the README. I don't understand if what you're trying to do isn't listed here?

kiyoon avatar Jan 18 '25 11:01 kiyoon

It's a reasonable idea, and I've tried to achieve it multiple times since I installed this plugin.

you can add:

goto_next_start = { ["]m"] = "@function.outer", }

But it would go to a sibling start, not the parent function, like the af or if does.

It would be great to have something like: goto_parent_start and goto_parent_end jumps.

Sorry I just read this. So instead of going forward you wanted to go backward. I guess you could use [m instead but this wouldn't work if you have an nested function.

Well, for such a customised behaviour it's inevitable to let you make a custom function. I don't think the suggested feature will be documented or implemented, but thanks for sharing your tip.

kiyoon avatar Jan 18 '25 14:01 kiyoon

It's a reasonable idea, and I've tried to achieve it multiple times since I installed this plugin. you can add: goto_next_start = { ["]m"] = "@function.outer", } But it would go to a sibling start, not the parent function, like the af or if does. It would be great to have something like: goto_parent_start and goto_parent_end jumps.

Sorry I just read this. So instead of going forward you wanted to go backward. I guess you could use [m instead but this wouldn't work if you have an nested function.

Well, for such a customised behaviour it's inevitable to let you make a custom function. I don't think the suggested feature will be documented or implemented, but thanks for sharing your tip.

vaf can work normally but like this [m cursor jump seems to be unable to jump properly in my lua file maybe it's related to some dialect like language I use

reimu1234 avatar Jan 19 '25 00:01 reimu1234

Just to be clear, the ["[m"] = "@function.outer", doesn't go to the parent function, but tho ANY start of a function, it can be a sibling or either be nested.

In this example, there are 3 functions, and I would have to press [m 3 times to reach the top level:

Image

And what we want is 1 action to reach the top level function, or to reach its end

carlos-algms avatar Jan 20 '25 16:01 carlos-algms

Just to be clear, the ["[m"] = "@function.outer", doesn't go to the parent function, but tho ANY start of a function, it can be a sibling or either be nested.

In this example, there are 3 functions, and I would have to press [m 3 times to reach the top level:

Image

And what we want is 1 action to reach the top level function, or to reach its end

I have used Prev_Sibling(). Thank you for your reminder

reimu1234 avatar Jan 21 '25 00:01 reimu1234

@carlos-algms Would something like this meet your need? (but it only works in normal mode)

nmap [m vamo<esc>

Where vam selects a function.

kiyoon avatar Jan 21 '25 00:01 kiyoon

That would accomplish the go-to step, but it seems to NOT set a jump point, so I can't go back with Ctrl-o, another side-effect is it would override my "last selected" mark, and gv wouldn't behave as expected.

But it's a neat solution.

carlos-algms avatar Jan 21 '25 15:01 carlos-algms

Maybe providing an API of the selection area would be useful, and would make it easier to test.

kiyoon avatar Jan 21 '25 23:01 kiyoon