Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: include surrounding whitespace #3

Closed
WieeRd opened this issue Jun 24, 2024 · 2 comments · Fixed by #4
Closed

feat: include surrounding whitespace #3

WieeRd opened this issue Jun 24, 2024 · 2 comments · Fixed by #4

Comments

@WieeRd
Copy link
Contributor

WieeRd commented Jun 24, 2024

Thanks for the useful plugin, being able to use the highlighted context as a textobject is really convenient.

I would like to request a feature/option to include surrounding whitespaces, like the builtin ap textobject.
In short, including the empty lines below the original selection target as a part of the textobject.
When the target is at the end of the buffer, empty lines above the target is included instead.

Example

foo
foo

bar <- cursor
bar

baz
baz

dap

foo
foo

baz <- cursor
baz

dap

foo
foo

Rationale

This is mostly intended for the require("treesitter_indent_object.textobj").select_indent_outer(true, "V").
Invoking a linewise indent-outer textobject means that you want to operate on the whole thing that has caused the indentation.
And it is very common to insert empty lines between functions definitions and control flow statements to improve readability.
After the function/control flow itself is gone, the separator empty line is no longer needed.

def foo(arg: str):
    if arg == "칼퇴근":  # <- cursor on this line
        return

    extra_work()
    imagine("happiness")

def imagine(arg: str):
    raise KeyError(arg)

For example, if I were to remove the guard clause with daI, I no longer need the empty line below it.
Similarly, daI on def imagine() should remove the empty line above it.
This is also useful for reordering functions, as you don't need to re-insert the extra empty line after pasting.

Existing Implementations

nvim-treesitter-textobjects has include_surrounding_whitespace option in its module config.
Related utility functions can be found at lua/nvim-treesitter/textobjects/select.lua

@kiyoon
Copy link
Owner

kiyoon commented Jun 24, 2024

Thanks for the great suggestion!

I remember the implementation in nvim-treesitter-textobject is quite messy because it had to deal with many different vim settings which are not even resolved yet (nvim-treesitter/nvim-treesitter-textobjects#488, nvim-treesitter/nvim-treesitter-textobjects#575)

I can't guarantee when I can actually support this, but in the meantime you can do the following:

      {
        "aI",
        function()
          require("treesitter_indent_object.textobj").select_indent_outer(true)
          local prev_cursor = vim.api.nvim_win_get_cursor(0)
          vim.cmd [[normal! w]]
          local cursor = vim.api.nvim_win_get_cursor(0)

          -- loop until line is not empty, or cursor didn't move
          local line = vim.fn.getline(cursor[1])
          while line:match "^%s*$" and cursor[1] ~= prev_cursor[1] do
            vim.cmd [[normal! )]]
            prev_cursor = { cursor[1], cursor[2] }
            cursor = vim.api.nvim_win_get_cursor(0)
            line = vim.fn.getline(cursor[1])
          end

          if cursor[1] ~= prev_cursor[1] then
            -- line is not empty. Adjust cursor position (col-1 or row-1)
            if cursor[2] == 0 then
              vim.cmd [[normal! k$]]
            else
              vim.cmd [[normal! h]]
            end
          end
        end,
        mode = { "x", "o" },
        desc = "Select context-aware indent (outer, line-wise, include trailing whitespaces)",
      },

Simply you can move around the cursor as you want after calling the function and it will be your new textobject.

Note it still has one problem where, when the indent level changes after the selection like below

    if True:
        pass  # cursor around here

if __name__ == "__main__":
    main()

After deleting with daI it will break the indentation

    if __name__ == "__main__":
    main()

Hint: if you want to resolve this issue, you can do vim.cmd [[normal! o]] and also try to delete the top of the selection.

@kiyoon
Copy link
Owner

kiyoon commented Jun 24, 2024

I already have a better idea. Let's just use line-wise selection because it's an indent textobject anyway.

      {
        "aI",
        function()
          require("treesitter_indent_object.textobj").select_indent_outer(true, "V")
          local prev_cursor = vim.api.nvim_win_get_cursor(0)
          vim.cmd [[normal! w]]
          local cursor = vim.api.nvim_win_get_cursor(0)

          -- loop until line is not empty, or cursor didn't move
          local line = vim.fn.getline(cursor[1])
          while line:match "^%s*$" and cursor[1] ~= prev_cursor[1] do
            vim.cmd [[normal! )]]
            prev_cursor = { cursor[1], cursor[2] }
            cursor = vim.api.nvim_win_get_cursor(0)
            line = vim.fn.getline(cursor[1])
          end

          if cursor[1] ~= prev_cursor[1] then
            -- line is not empty. Adjust cursor position (row-1)
            vim.cmd [[normal! k$]]
          end
        end,
        mode = { "x", "o" },
        desc = "Select context-aware indent (outer, line-wise)",
      },

We don't need to worry about the following indent mismatching issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants