feat: Allow to expand until certain condition is met
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:
- the api is very flexible but at the same time quite complex. I had to do it in this way because the
expand_allfunction will not expand the last directory. It stops as soon asshould_expandreturnsfalsebut I wanted the last directory to be expanded. - ~I had to copy
populate_childrenmethod which is not a part of public api~ replaced withexplorer.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.
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.
Any updates on this one @ghostbuster91 ?
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"))
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?
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
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.
Any idea how to fix it?
status is not a parameter for function Explorer:expand(node), looks like it's unused.
That parameter is not used and thus can be removed.
That parameter is not used and thus can be removed.
Right, I always forget that this is possible in dynamically-typed languages :+1:
Any idea how to fix it?
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:
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:
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 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.
Closing as this PR is stale. Please reopen if you wish to continue this work.

