mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-12 02:23:39 +08:00
506 lines
16 KiB
Lua
506 lines
16 KiB
Lua
|
-- This files holds code for scanning the filesystem to build the tree.
|
||
|
local uv = vim.loop
|
||
|
|
||
|
local renderer = require("neo-tree.ui.renderer")
|
||
|
local utils = require("neo-tree.utils")
|
||
|
local filter_external = require("neo-tree.sources.filesystem.lib.filter_external")
|
||
|
local file_items = require("neo-tree.sources.common.file-items")
|
||
|
local log = require("neo-tree.log")
|
||
|
local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch")
|
||
|
local git = require("neo-tree.git")
|
||
|
local events = require("neo-tree.events")
|
||
|
local async = require("plenary.async")
|
||
|
|
||
|
local Path = require("plenary.path")
|
||
|
local os_sep = Path.path.sep
|
||
|
|
||
|
local M = {}
|
||
|
|
||
|
local on_directory_loaded = function(context, dir_path)
|
||
|
local state = context.state
|
||
|
local scanned_folder = context.folders[dir_path]
|
||
|
if scanned_folder then
|
||
|
scanned_folder.loaded = true
|
||
|
end
|
||
|
if state.use_libuv_file_watcher then
|
||
|
local root = context.folders[dir_path]
|
||
|
if root then
|
||
|
local target_path = root.is_link and root.link_to or root.path
|
||
|
local fs_watch_callback = vim.schedule_wrap(function(err, fname)
|
||
|
if err then
|
||
|
log.error("file_event_callback: ", err)
|
||
|
return
|
||
|
end
|
||
|
if context.is_a_never_show_file(fname) then
|
||
|
-- don't fire events for nodes that are designated as "never show"
|
||
|
return
|
||
|
else
|
||
|
events.fire_event(events.FS_EVENT, { afile = target_path })
|
||
|
end
|
||
|
end)
|
||
|
|
||
|
log.trace("Adding fs watcher for ", target_path)
|
||
|
fs_watch.watch_folder(target_path, fs_watch_callback)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local dir_complete = function(context, dir_path)
|
||
|
local paths_to_load = context.paths_to_load
|
||
|
local folders = context.folders
|
||
|
|
||
|
on_directory_loaded(context, dir_path)
|
||
|
|
||
|
-- check to see if there are more folders to load
|
||
|
local next_path = nil
|
||
|
while #paths_to_load > 0 and not next_path do
|
||
|
next_path = table.remove(paths_to_load)
|
||
|
-- ensure that the path is still valid
|
||
|
local success, result = pcall(vim.loop.fs_stat, next_path)
|
||
|
-- ensure that the result is a directory
|
||
|
if success and result and result.type == "directory" then
|
||
|
-- ensure that it is not already loaded
|
||
|
local existing = folders[next_path]
|
||
|
if existing and existing.loaded then
|
||
|
next_path = nil
|
||
|
end
|
||
|
else
|
||
|
-- if the path doesn't exist, skip it
|
||
|
next_path = nil
|
||
|
end
|
||
|
end
|
||
|
return next_path
|
||
|
end
|
||
|
|
||
|
local render_context = function(context)
|
||
|
local state = context.state
|
||
|
local root = context.root
|
||
|
local parent_id = context.parent_id
|
||
|
|
||
|
if not parent_id and state.use_libuv_file_watcher and state.enable_git_status then
|
||
|
log.trace("Starting .git folder watcher")
|
||
|
local path = root.path
|
||
|
if root.is_link then
|
||
|
path = root.link_to
|
||
|
end
|
||
|
fs_watch.watch_git_index(path, require("neo-tree").config.git_status_async)
|
||
|
end
|
||
|
fs_watch.updated_watched()
|
||
|
|
||
|
if root and root.children then
|
||
|
file_items.deep_sort(root.children, state.sort_function_override)
|
||
|
end
|
||
|
if parent_id then
|
||
|
-- lazy loading a child folder
|
||
|
renderer.show_nodes(root.children, state, parent_id, context.callback)
|
||
|
else
|
||
|
-- full render of the tree
|
||
|
renderer.show_nodes({ root }, state, nil, context.callback)
|
||
|
end
|
||
|
|
||
|
context.state = nil
|
||
|
context.callback = nil
|
||
|
context.all_items = nil
|
||
|
context.root = nil
|
||
|
context.parent_id = nil
|
||
|
context = nil
|
||
|
end
|
||
|
|
||
|
local job_complete = function(context)
|
||
|
local state = context.state
|
||
|
local parent_id = context.parent_id
|
||
|
if #context.all_items == 0 then
|
||
|
log.info("No items, skipping git ignored/status lookups")
|
||
|
render_context(context)
|
||
|
return
|
||
|
end
|
||
|
if state.filtered_items.hide_gitignored or state.enable_git_status then
|
||
|
if require("neo-tree").config.git_status_async then
|
||
|
git.mark_ignored(state, context.all_items, function(all_items)
|
||
|
if parent_id then
|
||
|
vim.list_extend(state.git_ignored, all_items)
|
||
|
else
|
||
|
state.git_ignored = all_items
|
||
|
end
|
||
|
vim.schedule(function()
|
||
|
render_context(context)
|
||
|
end)
|
||
|
end)
|
||
|
return
|
||
|
else
|
||
|
local all_items = git.mark_ignored(state, context.all_items)
|
||
|
if parent_id then
|
||
|
vim.list_extend(state.git_ignored, all_items)
|
||
|
else
|
||
|
state.git_ignored = all_items
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
render_context(context)
|
||
|
end
|
||
|
|
||
|
local function create_node(context, node)
|
||
|
local success3, item = pcall(file_items.create_item, context, node.path, node.type)
|
||
|
end
|
||
|
|
||
|
local function process_node(context, path)
|
||
|
on_directory_loaded(context, path)
|
||
|
end
|
||
|
|
||
|
local function get_children_sync(path)
|
||
|
local children = {}
|
||
|
local success, dir = pcall(vim.loop.fs_opendir, path, nil, 1000)
|
||
|
if not success then
|
||
|
log.error("Error opening dir:", dir)
|
||
|
end
|
||
|
local success2, stats = pcall(vim.loop.fs_readdir, dir)
|
||
|
if success2 and stats then
|
||
|
for _, stat in ipairs(stats) do
|
||
|
local child_path = utils.path_join(path, stat.name)
|
||
|
table.insert(children, { path = child_path, type = stat.type })
|
||
|
end
|
||
|
end
|
||
|
pcall(vim.loop.fs_closedir, dir)
|
||
|
return children
|
||
|
end
|
||
|
|
||
|
local function get_children_async(path, callback)
|
||
|
uv.fs_opendir(path, function(_, dir)
|
||
|
uv.fs_readdir(dir, function(_, stats)
|
||
|
local children = {}
|
||
|
if stats then
|
||
|
for _, stat in ipairs(stats) do
|
||
|
local child_path = utils.path_join(path, stat.name)
|
||
|
table.insert(children, { path = child_path, type = stat.type })
|
||
|
end
|
||
|
end
|
||
|
uv.fs_closedir(dir)
|
||
|
callback(children)
|
||
|
end)
|
||
|
end, 1000)
|
||
|
end
|
||
|
|
||
|
local function scan_dir_sync(context, path)
|
||
|
process_node(context, path)
|
||
|
local children = get_children_sync(path)
|
||
|
for _, child in ipairs(children) do
|
||
|
create_node(context, child)
|
||
|
if child.type == "directory" then
|
||
|
local grandchild_nodes = get_children_sync(child.path)
|
||
|
if
|
||
|
grandchild_nodes == nil
|
||
|
or #grandchild_nodes == 0
|
||
|
or #grandchild_nodes == 1 and grandchild_nodes[1].type == "directory"
|
||
|
then
|
||
|
scan_dir_sync(context, child.path)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function scan_dir_async(context, path, callback)
|
||
|
get_children_async(path, function(children)
|
||
|
for _, child in ipairs(children) do
|
||
|
create_node(context, child)
|
||
|
if child.type == "directory" then
|
||
|
local grandchild_nodes = get_children_sync(child.path)
|
||
|
if
|
||
|
grandchild_nodes == nil
|
||
|
or #grandchild_nodes == 0
|
||
|
or #grandchild_nodes == 1 and grandchild_nodes[1].type == "directory"
|
||
|
then
|
||
|
scan_dir_sync(context, child.path)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
process_node(context, path)
|
||
|
callback(path)
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
-- async_scan scans all the directories in context.paths_to_load
|
||
|
-- and adds them as items to render in the UI.
|
||
|
local function async_scan(context, path)
|
||
|
log.trace("async_scan: ", path)
|
||
|
local scan_mode = require("neo-tree").config.filesystem.scan_mode
|
||
|
|
||
|
if scan_mode == "deep" then
|
||
|
local scan_tasks = {}
|
||
|
for _, p in ipairs(context.paths_to_load) do
|
||
|
local scan_task = async.wrap(function(callback)
|
||
|
scan_dir_async(context, p, callback)
|
||
|
end, 1)
|
||
|
table.insert(scan_tasks, scan_task)
|
||
|
end
|
||
|
|
||
|
async.util.run_all(
|
||
|
scan_tasks,
|
||
|
vim.schedule_wrap(function()
|
||
|
job_complete(context)
|
||
|
end)
|
||
|
)
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- scan_mode == "shallow"
|
||
|
context.directories_scanned = 0
|
||
|
context.directories_to_scan = #context.paths_to_load
|
||
|
|
||
|
context.on_exit = vim.schedule_wrap(function()
|
||
|
job_complete(context)
|
||
|
end)
|
||
|
|
||
|
-- from https://github.com/nvim-lua/plenary.nvim/blob/master/lua/plenary/scandir.lua
|
||
|
local function read_dir(current_dir, ctx)
|
||
|
uv.fs_opendir(current_dir, function(err, dir)
|
||
|
if err then
|
||
|
log.error(current_dir, ": ", err)
|
||
|
return
|
||
|
end
|
||
|
local function on_fs_readdir(err, entries)
|
||
|
if err then
|
||
|
log.error(current_dir, ": ", err)
|
||
|
return
|
||
|
end
|
||
|
if entries then
|
||
|
for _, entry in ipairs(entries) do
|
||
|
local success, item = pcall(
|
||
|
file_items.create_item,
|
||
|
ctx,
|
||
|
utils.path_join(current_dir, entry.name),
|
||
|
entry.type
|
||
|
)
|
||
|
if success then
|
||
|
if ctx.recursive and item.type == "directory" then
|
||
|
ctx.directories_to_scan = ctx.directories_to_scan + 1
|
||
|
table.insert(ctx.paths_to_load, item.path)
|
||
|
end
|
||
|
else
|
||
|
log.error("error creating item for ", path)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
uv.fs_readdir(dir, on_fs_readdir)
|
||
|
return
|
||
|
end
|
||
|
uv.fs_closedir(dir)
|
||
|
on_directory_loaded(ctx, current_dir)
|
||
|
ctx.directories_scanned = ctx.directories_scanned + 1
|
||
|
if ctx.directories_scanned == #ctx.paths_to_load then
|
||
|
ctx.on_exit()
|
||
|
end
|
||
|
|
||
|
--local next_path = dir_complete(ctx, current_dir)
|
||
|
--if next_path then
|
||
|
-- local success, error = pcall(read_dir, next_path)
|
||
|
-- if not success then
|
||
|
-- log.error(next_path, ": ", error)
|
||
|
-- end
|
||
|
--else
|
||
|
-- on_exit()
|
||
|
--end
|
||
|
end
|
||
|
|
||
|
uv.fs_readdir(dir, on_fs_readdir)
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
--local first = table.remove(context.paths_to_load)
|
||
|
--local success, err = pcall(read_dir, first)
|
||
|
--if not success then
|
||
|
-- log.error(first, ": ", err)
|
||
|
--end
|
||
|
for i = 1, context.directories_to_scan do
|
||
|
read_dir(context.paths_to_load[i], context)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function sync_scan(context, path_to_scan)
|
||
|
log.trace("sync_scan: ", path_to_scan)
|
||
|
local scan_mode = require("neo-tree").config.filesystem.scan_mode
|
||
|
if scan_mode == "deep" then
|
||
|
for _, path in ipairs(context.paths_to_load) do
|
||
|
scan_dir_sync(context, path)
|
||
|
-- scan_dir(context, path)
|
||
|
end
|
||
|
job_complete(context)
|
||
|
else -- scan_mode == "shallow"
|
||
|
local success, dir = pcall(vim.loop.fs_opendir, path_to_scan, nil, 1000)
|
||
|
if not success then
|
||
|
log.error("Error opening dir:", dir)
|
||
|
end
|
||
|
local success2, stats = pcall(vim.loop.fs_readdir, dir)
|
||
|
if success2 and stats then
|
||
|
for _, stat in ipairs(stats) do
|
||
|
local path = utils.path_join(path_to_scan, stat.name)
|
||
|
local success3, item = pcall(file_items.create_item, context, path, stat.type)
|
||
|
if success3 then
|
||
|
if context.recursive and stat.type == "directory" then
|
||
|
table.insert(context.paths_to_load, path)
|
||
|
end
|
||
|
else
|
||
|
log.error("error creating item for ", path)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
vim.loop.fs_closedir(dir)
|
||
|
|
||
|
local next_path = dir_complete(context, path_to_scan)
|
||
|
if next_path then
|
||
|
sync_scan(context, next_path)
|
||
|
else
|
||
|
job_complete(context)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
M.get_items_sync = function(state, parent_id, path_to_reveal, callback)
|
||
|
return M.get_items(state, parent_id, path_to_reveal, callback, false)
|
||
|
end
|
||
|
|
||
|
M.get_items_async = function(state, parent_id, path_to_reveal, callback)
|
||
|
M.get_items(state, parent_id, path_to_reveal, callback, true)
|
||
|
end
|
||
|
|
||
|
M.get_items = function(state, parent_id, path_to_reveal, callback, async, recursive)
|
||
|
if state.async_directory_scan == "always" then
|
||
|
async = true
|
||
|
elseif state.async_directory_scan == "never" then
|
||
|
async = false
|
||
|
elseif type(async) == "nil" then
|
||
|
async = (state.async_directory_scan == "auto") or state.async_directory_scan
|
||
|
end
|
||
|
|
||
|
if not parent_id then
|
||
|
M.stop_watchers(state)
|
||
|
end
|
||
|
local context = file_items.create_context()
|
||
|
context.state = state
|
||
|
context.parent_id = parent_id
|
||
|
context.path_to_reveal = path_to_reveal
|
||
|
context.recursive = recursive
|
||
|
context.callback = callback
|
||
|
|
||
|
-- Create root folder
|
||
|
local root = file_items.create_item(context, parent_id or state.path, "directory")
|
||
|
root.name = vim.fn.fnamemodify(root.path, ":~")
|
||
|
root.loaded = true
|
||
|
root.search_pattern = state.search_pattern
|
||
|
context.root = root
|
||
|
context.folders[root.path] = root
|
||
|
state.default_expanded_nodes = state.force_open_folders or { state.path }
|
||
|
|
||
|
if state.search_pattern then
|
||
|
local search_opts = {
|
||
|
filtered_items = state.filtered_items,
|
||
|
find_command = state.find_command,
|
||
|
limit = state.search_limit or 50,
|
||
|
path = root.path,
|
||
|
term = state.search_pattern,
|
||
|
find_args = state.find_args,
|
||
|
find_by_full_path_words = state.find_by_full_path_words,
|
||
|
fuzzy_finder_mode = state.fuzzy_finder_mode,
|
||
|
on_insert = function(err, path)
|
||
|
if err then
|
||
|
log.debug(err)
|
||
|
else
|
||
|
file_items.create_item(context, path)
|
||
|
end
|
||
|
end,
|
||
|
on_exit = vim.schedule_wrap(function()
|
||
|
job_complete(context)
|
||
|
end),
|
||
|
}
|
||
|
if state.use_fzy then
|
||
|
filter_external.fzy_sort_files(search_opts, state)
|
||
|
else
|
||
|
-- Use the external command because the plenary search is slow
|
||
|
filter_external.find_files(search_opts)
|
||
|
end
|
||
|
else
|
||
|
-- In the case of a refresh or navigating up, we need to make sure that all
|
||
|
-- open folders are loaded.
|
||
|
local path = parent_id or state.path
|
||
|
context.paths_to_load = {}
|
||
|
if parent_id == nil then
|
||
|
if utils.truthy(state.force_open_folders) then
|
||
|
for _, f in ipairs(state.force_open_folders) do
|
||
|
table.insert(context.paths_to_load, f)
|
||
|
end
|
||
|
elseif state.tree then
|
||
|
context.paths_to_load = renderer.get_expanded_nodes(state.tree, state.path)
|
||
|
end
|
||
|
-- Ensure that there are no nested files in the list of folders to load
|
||
|
context.paths_to_load = vim.tbl_filter(function(p)
|
||
|
local stats = vim.loop.fs_stat(p)
|
||
|
return stats and stats.type == "directory"
|
||
|
end, context.paths_to_load)
|
||
|
if path_to_reveal then
|
||
|
-- be sure to load all of the folders leading up to the path to reveal
|
||
|
local path_to_reveal_parts = utils.split(path_to_reveal, utils.path_separator)
|
||
|
table.remove(path_to_reveal_parts) -- remove the file name
|
||
|
-- add all parent folders to the list of paths to load
|
||
|
utils.reduce(path_to_reveal_parts, "", function(acc, part)
|
||
|
local current_path = utils.path_join(acc, part)
|
||
|
if #current_path > #path then -- within current root
|
||
|
table.insert(context.paths_to_load, current_path)
|
||
|
table.insert(state.default_expanded_nodes, current_path)
|
||
|
end
|
||
|
return current_path
|
||
|
end)
|
||
|
context.paths_to_load = utils.unique(context.paths_to_load)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local filtered_items = state.filtered_items or {}
|
||
|
context.is_a_never_show_file = function(fname)
|
||
|
if fname then
|
||
|
local _, name = utils.split_path(fname)
|
||
|
if name then
|
||
|
if filtered_items.never_show and filtered_items.never_show[name] then
|
||
|
return true
|
||
|
end
|
||
|
if utils.is_filtered_by_pattern(filtered_items.never_show_by_pattern, fname, name) then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
table.insert(context.paths_to_load, path)
|
||
|
if async then
|
||
|
async_scan(context, path)
|
||
|
else
|
||
|
sync_scan(context, path)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
M.stop_watchers = function(state)
|
||
|
if state.use_libuv_file_watcher and state.tree then
|
||
|
-- We are loaded a new root or refreshing, unwatch any folders that were
|
||
|
-- previously being watched.
|
||
|
local loaded_folders = renderer.select_nodes(state.tree, function(node)
|
||
|
return node.type == "directory" and node.loaded
|
||
|
end)
|
||
|
fs_watch.unwatch_git_index(state.path, require("neo-tree").config.git_status_async)
|
||
|
for _, folder in ipairs(loaded_folders) do
|
||
|
log.trace("Unwatching folder ", folder.path)
|
||
|
if folder.is_link then
|
||
|
fs_watch.unwatch_folder(folder.link_to)
|
||
|
else
|
||
|
fs_watch.unwatch_folder(folder:get_id())
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
log.debug(
|
||
|
"Not unwatching folders... use_libuv_file_watcher is ",
|
||
|
state.use_libuv_file_watcher,
|
||
|
" and state.tree is ",
|
||
|
utils.truthy(state.tree)
|
||
|
)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return M
|