nvim-treesitter / nvim-treesitter/nvim-treesitter-textobjects

feat: text object subsets (move/select/delete)

Open
#471 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Tree-sitter Query
Stars
2.8k
Forks
271
Avg merge
8d 8h
Merged PRs (30d)
1

Description

I'd like to be able to (move/select/delete) the parent node without (m/s/d)ing the contents - for changing code quickly

Describe the solution you'd like
Example:

"@function.outer _ @function.inner"

Would select the function declaration without its body

Alternatives
Here's some code I've written which deletes the function while preserving the contents (and deindenting them)

function delete_function_declaration_lines()
    funcrowStart, funccolStart, funcrowEnd, funccolEnd = function_range_surrounding_cursor()
    bodyrowStart, bodycolStart, bodyrowEnd, bodycolEnd = function_body_range_surrounding_cursor()
    functionStartDelta = bodyrowStart - funcrowStart
    functionEndDelta = funcrowEnd - bodyrowEnd
    -- print("functionEndDelta: " .. tostring(functionEndDelta))
    if funcrowStart == bodyrowStart then
        print("WARN: function start and body start are on the same line. Unable to do interpolation")
        return nil
    end
    if funcrowEnd == bodyrowEnd then
        -- Then go into column mode
        -- vim.api.nvim_buf_set_text(buf, startLine - 1, startColumn - 1, endLine, endColumn, lines)
        -- XXX probably can handle brace syntax here
        print("WARN: function end and body end are on the same line. Unable to do interpolation")
        return nil
    end
    -- Delete the end first to preverse the line numbers
    vim.api.nvim_buf_set_lines(0, bodyrowEnd+1, bodyrowEnd+functionEndDelta+1, false, {})
    -- Deindent - convert from index to line number
    deindent_lines(bodyrowStart+1, bodyrowEnd+1)
    -- delete the beginning second
    vim.api.nvim_buf_set_lines(0, funcrowStart, funcrowStart+functionStartDelta, false, {})
end


-- < Retrieve the name of the function the cursor is in.
function function_surrounding_cursor()
    ts_utils = require('nvim-treesitter.ts_utils')
    if not ts_utils then
        return "<tsutils>"
    end
    local current_node = ts_utils.get_node_at_cursor()

    if not current_node then
        return "<none1>"
    end

    local func = current_node

    while func do
        if func:type() == 'function_declaration' then
            break
        end

        func = func:parent()
    end

    if not func then
        prev_function_node = nil
        prev_function_name = ""
        return "<none>"
    end

    if func == prev_function_node then
        return prev_function_name
    end

    prev_function_node = func

    local find_name
    find_name = function(node)
        for i = 0, node:named_child_count() - 1, 1 do
            print("i :" .. tostring(i))
            local child = node:named_child(i)
            local type = child:type()
            print("child type: " .. type)

            if type == 'identifier' or type == 'operator_name' then
                return vim.treesitter.get_node_text(child, 0)
            else
                local name = find_name(child)

                if name then
                    return name
                end
            end
        end

        return nil
    end

    prev_function_name = find_name(func)
    return prev_function_name
end

function function_range_surrounding_cursor()
    -- XXX ok so the issue I'm having is that
    -- The retured range is inccorect from
    -- using ts_utils

    -- require "nvim-treesitter.parsers"
    ts_utils = require('nvim-treesitter.ts_utils')
    local current_node = ts_utils.get_node_at_cursor()
    if not current_node then
        return ""
    end
    local func = current_node
    while func do
        if func:type() == 'function_declaration' then
            break
        end

        func = func:parent()
    end

    if not func then
        prev_function_node = nil
        prev_function_name = ""
        return "<none>"
    end

    if func == prev_function_node then
        return prev_function_name
    end

    print(func:type())
    return vim.treesitter.get_node_range(func, 0)
    -- return func:range()
end

-- print("DONT ME")
function test_me()
    print("HI")
end
-- print("DONT ME2")


function generateRange(startNum, endNum)
  local range = {}
  for i = startNum, endNum do
    table.insert(range, i)
  end
  return range
end

function printTable(table_)
    for _, item in ipairs(table_) do
        print(item)
    end
end

function deindent_lines(startRow, endRow)
    -- arguments are line numbers and not an index

    -- startRow and endRow are inclusive
    -- Get the current buffer handle
    local buf = vim.api.nvim_get_current_buf()

    -- Get the shiftwidth value (number of spaces per tabstop)
    local shiftwidth = vim.api.nvim_buf_get_option(buf, 'shiftwidth')

    -- Get the lines to be deindented
    local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)

    -- Deindent the specified lines by the tabstop value
    local range = generateRange(startRow, endRow)
    for _, lineNum in ipairs(range) do
      local line = lines[lineNum]
      local deindentedLine = line:gsub("^" .. string.rep(" ", shiftwidth), "")
      lines[lineNum] = deindentedLine
    end

    -- Update the buffer with the modified lines
    vim.api.nvim_buf_set_text(buf, 0, 0, -1, 0, lines)
end


function function_body_range_surrounding_cursor()
    ts_utils = require('nvim-treesitter.ts_utils')
    local current_node = ts_utils.get_node_at_cursor()
    if not current_node then
        return ""
    end
    local func = current_node
    while func do
        if func:type() == 'function_declaration' then
            break
        end

        func = func:parent()
    end

    if not func then
        prev_function_node = nil
        prev_function_name = ""
        return "<none>"
    end

    if func == prev_function_node then
        return prev_function_name
    end

    local find_body
    find_body = function(node)
        for i = 0, node:named_child_count() - 1, 1 do
            -- print("i :" .. tostring(i))
            local child = node:named_child(i)
            local type = child:type()
            -- print("child type: " .. type)
            if type == "block" then
                return vim.treesitter.get_node_range(child, 0)
            end
        end
        return nil
    end
    return find_body(func)
end

Additional context
Add any other context or screenshots about the feature request here.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the repository's existing textobject selection, move, and delete entry points, then compare them with the Lua and Tree-sitter example in the issue. Done means parent nodes can be acted on without applying the same operation to their contents, including the shown function-declaration case.

Written by the indexing model from the issue text.

Assessment

Tech stack
lua, neovim
Domain
tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.