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: add util function include_surrounding_empty_lines() #4

Merged
merged 3 commits into from
Jul 8, 2024

Conversation

WieeRd
Copy link
Contributor

@WieeRd WieeRd commented Jul 5, 2024

Fixes #3.

After an embarrassing amount of off-by-one errors, I have created a more robust version of selection expansion.

---Expand the linewise visual selection to the surrounding empty lines.
M.include_surrounding_empty_lines = function()
  local cur = vim.fn.line "."
  local eob = vim.fn.line "$"

  -- expand the selection to the empty lines below
  local i = cur
  while i < eob and vim.fn.getline(i + 1):match "^%s*$" ~= nil do
    i = i + 1
  end
  vim.api.nvim_win_set_cursor(0, { i, 99999 })

  -- if: there were no empty lines below (cursor did not move)
  -- or *only* empty lines were below (cursor at the last line)
  if i ~= cur and i ~= eob then
    return
  end

  -- then: also expand the selection to the empty lines above
  vim.cmd [[normal! o]]
  local i = vim.fn.line "."
  while i > 1 and vim.fn.getline(i - 1):match "^%s*$" ~= nil do
    i = i - 1
  end
  vim.api.nvim_win_set_cursor(0, { i, 0 })
  vim.cmd [[normal! o]]
end
{
  "aI",
  function()
    require'treesitter_indent_object.textobj'.select_indent_outer(true, 'V')
    require'treesitter_indent_object.utils'.include_surrounding_empty_lines()
  end,
  mode = { "x", "o" },
}

It can expand the selection upwards as well as downwards when it makes sense to do so.
Here's an example of how daI would function after applying this.

Downwards

  def foo():
      pass

- def bar(): # <- cursor
-     pass
-
  def baz():
      pass

Upwards

  def foo():
      pass
-
- def bar(): # <- cursor
-     pass

Trailing empty lines

Expands both way if there are no further contents after the empty lines below.
This behavior can be observed from the builtin ap as well. I never knew this until now.

  def foo():
      pass
-
- def bar(): # <- cursor
-     pass
-
-

Expand upwards if cannot go down at all

I find this particularly useful. No other textobject can so precisely do what I want in this situation.

 fn example() {
     foo();
-
-    if bar() { // <- cursor
-        baz();
-    }
 }

@WieeRd
Copy link
Contributor Author

WieeRd commented Jul 5, 2024

It was quite awkward to make this an option/parameter due to how it only works with outer-linewise mode specifically.
However I think it is useful and robust enough to be included in the plugin code itself.
So this PR just shoves it in the utils module and adds a tips and tricks section in the README.
(and slight edits to the NOTE: section if you don't mind)
There may be a better way, I'm not sure.

@kiyoon
Copy link
Owner

kiyoon commented Jul 6, 2024

Thanks for your contribution with clean documentation and implementation!

Do you think it's better to put it in another directory, called refiner instead of utils?
That way it may be more obvious for the users because none of the other functions in utils are actually meant for public use.

I'm down with refiner, postprocess etc.

@WieeRd
Copy link
Contributor Author

WieeRd commented Jul 6, 2024

Well I only put it in utils mainly because I had no idea where to.
Now that you mention it it was indeed a bad placement as people may mistakenly expect other functions in the utils to be also usable.

But before we talk about naming a separate module, I'm curious about why you decided to expose the core functionality through "treesitter_indent_object.textobj" (whopping 32 characters) and not the main module "treesitter_indent_object" (still 25, can we get an alias like ts-indentobj).

Plugins typically tend to expose all the major APIs at the top level main module by re-exporting important bits from the nested submodules. Usually the nested modules are "accessible, but private by convention" - direct access is only done to gain more control and hack the plugin but at the risk of being prone to announced internal breaking changes.

So I would personally prefer having all the end user APIs re-exported at the top.

As for the actual location refiner sounds fine. That or I think just putting it in the textobj module with the selection APIs is also reasonable.

@kiyoon
Copy link
Owner

kiyoon commented Jul 6, 2024

By the time I wrote it I had no idea, but even now I'm not so sure! Do plugins actually expose main functions on the top level?

I think I just thought that putting it in a module textobj is intuitive for the users reading the code. We can put the textobjects on the top level but I think the refiner function is slightly niche and thus it is also good to have it in a module (not top level)

@WieeRd
Copy link
Contributor Author

WieeRd commented Jul 7, 2024

It's just a tendency I found while inspecting plugins I use(d).
There is no real standard in Neovim/Lua and it's just a convention for convenience purpose.
Some plugins do occasionally deviate from this convention so not a strict rule.
The decision is totally up to you and the plugin is usable as long as it is properly documented.

@kiyoon
Copy link
Owner

kiyoon commented Jul 7, 2024

Let's use the name refiner. Please move the function and let's not change the location of the existing functions. Thanks for the opinion!

@WieeRd
Copy link
Contributor Author

WieeRd commented Jul 8, 2024

:)

@kiyoon
Copy link
Owner

kiyoon commented Jul 8, 2024

Thank you! This is truly amazing and I also put it in my config.

@kiyoon kiyoon merged commit 84feec8 into kiyoon:master Jul 8, 2024
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 this pull request may close these issues.

feat: include surrounding whitespace
2 participants