nvim-tree.lua icon indicating copy to clipboard operation
nvim-tree.lua copied to clipboard

feat: Allow to expand until certain condition is met

Open ghostbuster91 opened this issue 1 year ago • 13 comments

This is my attempt to solve #2789

With this change I can now define the following mapping:

local function stop_expansion(_, node)
    return false, stop_expansion
end
local function expand_until_non_single(count, node)
    local cwd = node.link_to or node.absolute_path
    local handle = vim.loop.fs_scandir(cwd)
    if not handle then
        return false, stop_expansion
    end
    local status = git.load_project_status(cwd)
    populate_children(handle, cwd, node, status)
    local child_folder_only = explorer_node.has_one_child_folder(node) and node.nodes[1]
    if count > 1 and not child_folder_only then
        return true, stop_expansion
    elseif child_folder_only then
        return true, expand_until_non_single
    else
        return false, stop_expansion
    end
end
map('n', 'E', function()
    api.tree.expand_all(nil, expand_until_non_single)
end, opts("Expand until not single"))

This works as expected however it has some downsides:

  1. the api is very flexible but at the same time quite complex. I had to do it in this way because the expand_all function will not expand the last directory. It stops as soon as should_expand returns false but I wanted the last directory to be expanded.
  2. ~I had to copy populate_children method which is not a part of public api~ replaced with explorer.explore(node, status)

Btw if this got accepted in some form I am going to replace the default expand behavior in my config with this, since I find it so useful.

ghostbuster91 avatar Jun 01 '24 13:06 ghostbuster91

I love the concept and it is something we should do. Tried your example after a lot of hacking and it looks to work nicely.

  • [x] Does nothing when called from the root node
  • [x] Needs documentation

Use of private functions in your example aside, this is far too complex and not approachable for users. Having to populate the children is the key problem.

Suggestion: come up with a simple use case and example - one that would go in the documentation. We can start simplifying from there.

Perhaps I'm misunderstanding: why does this need to be an iterator? I was imagining the user would supply a simple boolean returning function to test against a node.

alex-courtis avatar Jun 07 '24 04:06 alex-courtis

Any updates on this one @ghostbuster91 ?

alex-courtis avatar Aug 25 '24 02:08 alex-courtis

Sorry, I was quite busy. I am going to take a look at it.

I love the concept and it is something we should do.

I am glad to hear that!

Perhaps I'm misunderstanding: why does this need to be an iterator? I was imagining the user would supply a simple boolean returning function to test against a node.

Because the simple fn: node => boolean is insufficient in my case. Take a look at the following example:

a
└── b
    └── c
        ├── c2
        │   └── d2 (file)
        └── d (file)

When expanding a I would like to expand b and c. However c has multiple children and also directories inside so it wouldn't pass a simple should_expand function.

In my case the condition is following: Keep expanding until you reach a node that has more than one children but expand this node as well. In other words the expansion is inclusive.

Perhaps this could be expressed in simpler way for example by using a flag - inclusive=true on the user side, however I thought that using this approach would be more elastic and elegant. Though I am not attach to it, so if you have an idea how to simplify it I will happily implement it.

come up with a simple use case and example - one that would go in the documentation.

update:

I would see it as:


local function expand_one_more(node)
   return {expand = true, fn_next = stop_expansion}
end

local function expand_until_non_single(node)
    local has_one_child_folder = explorer_node.has_one_child_folder(node)
    if has_one_child_folder then
        return {expand = true, fn_next = expand_until_non_single}
    else
        return {expand = false, fn_next = expand_one_more}
end

local f = function(node)
    if node.open then
        lib.expand_or_collapse(node, nil)
    else
        if node.nodes then
            api.tree.expand_all(node, expand_until_non_single)
        else
            edit("edit", node)
        end
    end
end

map('n', '<CR>', wrap_node(f), opts("Expand until not single or collapse"))

ghostbuster91 avatar Sep 01 '24 15:09 ghostbuster91

In the end I was able to simply replace the iterator with accessing node's siblings via node.parent.nodes. With this the user side function for my particular use case looks as follow:

local function expand_until_non_single(count, node, populate_node)
    populate_node(node)
    if node.nodes == nil or not node.parent.open then
        return false
    end
    local has_one_child_folder = explorer_node.has_one_child_folder(node)
    local has_no_siblings = explorer_node.has_one_child_folder(node.parent)
    if has_one_child_folder and count == 0 then
        return true
    elseif has_no_siblings then
        return true
    else
        return false
    end
end

The CI keeps failing with following error:

{
    "file:///home/runner/work/nvim-tree.lua/nvim-tree.lua/lua/nvim-tree/actions/tree/modifiers/expand-all.lua": [
        {
            "code": "redundant-parameter",
            "message": "This function expects a maximum of 2 argument(s) but instead it is receiving 3.",
            "range": {
                "end": {
                    "character": 43,
                    "line": 33
                },
                "start": {
                    "character": 37,
                    "line": 33
                }
            },
            "severity": 2,
            "source": "Lua Diagnostics."
        }
    ]
}

Any idea how to fix it?

ghostbuster91 avatar Sep 01 '24 20:09 ghostbuster91

This minimal setup worked nicely.

I've changed things a bit to use more api and less internals.

However, it's still using lib and explorer_node which are internals, subject to change at any time. They most definitely are changing during https://github.com/orgs/nvim-tree/projects/1

The user must be able to write their function via API only.

vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1

vim.cmd([[set runtimepath=$VIMRUNTIME]])
vim.cmd([[set packpath=/tmp/nvt-min/site]])
local package_root = "/tmp/nvt-min/site/pack"
local install_path = package_root .. "/packer/start/packer.nvim"
local function load_plugins()
  require("packer").startup({
    {
      "wbthomason/packer.nvim",
      "nvim-tree/nvim-tree.lua",
      "nvim-tree/nvim-web-devicons",
      -- ADD PLUGINS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE
    },
    config = {
      package_root = package_root,
      compile_path = install_path .. "/plugin/packer_compiled.lua",
      display = { non_interactive = true },
    },
  })
end
if vim.fn.isdirectory(install_path) == 0 then
  print("Installing nvim-tree and dependencies.")
  vim.fn.system({ "git", "clone", "--depth=1", "https://github.com/wbthomason/packer.nvim", install_path })
end
load_plugins()
require("packer").sync()
vim.cmd([[autocmd User PackerComplete ++once echo "Ready!" | lua setup()]])
vim.opt.termguicolors = true
vim.opt.cursorline = true

local api = require("nvim-tree.api")
local lib = require("nvim-tree.lib")
local explorer_node = require("nvim-tree.explorer.node")

local function expand_until_non_single(count, node, populate_node)
  populate_node(node)
  if node.nodes == nil or not node.parent.open then
    return false
  end
  local has_one_child_folder = explorer_node.has_one_child_folder(node)
  local has_no_siblings = explorer_node.has_one_child_folder(node.parent)
  if has_one_child_folder and count == 0 then
    return true
  elseif has_no_siblings then
    return true
  else
    return false
  end
end

local function expand_until()
  local node = api.tree.get_node_under_cursor()
  if node.open then
    lib.expand_or_collapse(node, nil)
  else
    if node.nodes then
      api.tree.expand_all(node, expand_until_non_single)
    else
      api.node.open.edit(node)
    end
  end
end

local function on_attach(bufnr)
  local function opts(desc)
    return { desc = "nvim-tree: " .. desc, buffer = bufnr, noremap = true, silent = true, nowait = true }
  end

  api.config.mappings.default_on_attach(bufnr)

  vim.keymap.set("n", "O", expand_until, opts("Expand until"))
end

_G.setup = function()
  require("nvim-tree").setup({
    on_attach = on_attach,
  })
end

alex-courtis avatar Sep 02 '24 02:09 alex-courtis

Suggestions:

lib.expand_or_collapse(node, nil)

Replace with node.open.edit after testing for children i.e. node.nodes

  local has_one_child_folder = explorer_node.has_one_child_folder(node)
  local has_no_siblings = explorer_node.has_one_child_folder(node.parent)

Write your own test in this case. It will be a lot simpler as you have tested most of the conditions already.

alex-courtis avatar Sep 02 '24 02:09 alex-courtis

Any idea how to fix it?

20240902_121129

status is not a parameter for function Explorer:expand(node), looks like it's unused.

alex-courtis avatar Sep 02 '24 02:09 alex-courtis

20240902_121527

That parameter is not used and thus can be removed.

alex-courtis avatar Sep 02 '24 02:09 alex-courtis

20240902_121527

That parameter is not used and thus can be removed.

Right, I always forget that this is possible in dynamically-typed languages :+1:

ghostbuster91 avatar Sep 03 '24 07:09 ghostbuster91

Any idea how to fix it?

20240902_121129

status is not a parameter for function Explorer:expand(node), looks like it's unused.

It must have changed once I rebased on latest master. Fixed :+1:

ghostbuster91 avatar Sep 03 '24 09:09 ghostbuster91

With all these changes my custom should expand function looks as follow:

local function has_one_child_folder(node)
    return #node.nodes == 1 and node.nodes[1].nodes and
        vim.loop.fs_access(node.nodes[1].absolute_path, "R") or false
end

local function expand_until_non_single(_, node, populate_node)
    populate_node(node)
    if node.nodes == nil or not node.parent.open then
        return false
    end
    return has_one_child_folder(node.parent)
end

Thank you for helping me getting this PR across the finish line. I truly appreciate your time and attitude :bowing_man:

ghostbuster91 avatar Sep 03 '24 09:09 ghostbuster91

status is not a parameter for function Explorer:expand(node), looks like it's unused.

It must have changed once I rebased on latest master. Fixed 👍

Sorry about that; we are doing some significant refactoring of explorer/reload etc.

alex-courtis avatar Sep 07 '24 03:09 alex-courtis

@alex-courtis just fyi: I didn't forget about this. Just trying to finish my new apartment, so I can finally move in. I will try to finish this soon.

ghostbuster91 avatar Oct 09 '24 09:10 ghostbuster91

Closing as this PR is stale. Please reopen if you wish to continue this work.

alex-courtis avatar Jan 27 '25 00:01 alex-courtis