From 6662b60a2bb55f0c1eca4db54cba5c57c2b2daff Mon Sep 17 00:00:00 2001 From: Kiyan Date: Sat, 27 Nov 2021 16:02:54 +0100 Subject: [PATCH] feat/chore: rewrite git with job and some other fixes (#743) * feat/chore: rewrite git with job and some other fixes * fix: fs clear window, rename echo_warning -> warn also fix renaming and add an event blocker to avoid running many events at the same time --- README.md | 10 +- doc/nvim-tree-lua.txt | 39 +++++-- lua/nvim-tree.lua | 29 ++--- lua/nvim-tree/config.lua | 6 - lua/nvim-tree/fs.lua | 21 +++- lua/nvim-tree/git.lua | 168 --------------------------- lua/nvim-tree/git/init.lua | 86 ++++++++++++++ lua/nvim-tree/git/runner.lua | 99 ++++++++++++++++ lua/nvim-tree/git/utils.lua | 54 +++++++++ lua/nvim-tree/lib.lua | 199 ++++++++++++++++---------------- lua/nvim-tree/populate.lua | 78 +++++-------- lua/nvim-tree/renderer/init.lua | 4 +- lua/nvim-tree/utils.lua | 6 +- lua/nvim-tree/view.lua | 3 + plugin/nvim-tree-startup.lua | 5 +- 15 files changed, 453 insertions(+), 354 deletions(-) delete mode 100644 lua/nvim-tree/git.lua create mode 100644 lua/nvim-tree/git/init.lua create mode 100644 lua/nvim-tree/git/runner.lua create mode 100644 lua/nvim-tree/git/utils.lua diff --git a/README.md b/README.md index e5acadcb..722f0499 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ Install with [packer](https://github.com/wbthomason/packer.nvim): ```lua use { 'kyazdani42/nvim-tree.lua', - requires = 'kyazdani42/nvim-web-devicons', + requires = { + 'kyazdani42/nvim-web-devicons', -- optional, for file icon + }, config = function() require'nvim-tree'.setup {} end } ``` @@ -72,6 +74,11 @@ require'nvim-tree'.setup { dotfiles = false, custom = {} }, + git = { + enable = true, + ignore = true, + timeout = 500, + }, view = { width = 30, height = 30, @@ -89,7 +96,6 @@ require'nvim-tree'.setup { These additional options must be set **BEFORE** calling `require'nvim-tree'` or calling setup. They are being migrated to the setup function bit by bit, check [this issue](https://github.com/kyazdani42/nvim-tree.lua/issues/674) if you encounter any problems related to configs not working after update. ```vim -let g:nvim_tree_gitignore = 1 "0 by default let g:nvim_tree_quit_on_open = 1 "0 by default, closes the tree when you open a file let g:nvim_tree_indent_markers = 1 "0 by default, this option shows indent markers when folders are open let g:nvim_tree_git_hl = 1 "0 by default, will enable file highlight for git attributes (can be used without the icons). diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index faf2438f..646d92e0 100644 --- a/doc/nvim-tree-lua.txt +++ b/doc/nvim-tree-lua.txt @@ -101,6 +101,10 @@ function. cmd = nil, args = {} }, + git = { + enable = true, + ignore = true, + }, view = { width = 30, height = 30, @@ -231,6 +235,31 @@ Here is a list of the options available in the setup call: - `NvimTreeLspDiagnosticsInformation` - `NvimTreeLspDiagnosticsHint` +*nvim-tree.git* +- |git|: git integration with icons and colors + + - |git.enable|: enable / disable the feature + type: `boolean` + default: `true` + + - |git.ignore|: ignore files based on `.gitignore`. + will add `ignored=matching` to the integration when `true`. Otherwise will + add `ignored=no` to the integration which can lead to better performance. + + - |git.timeout|: kills the git process after some time if it takes too long + type: `number` + default: `400` (ms) + + You will still need to configure `g:nvim_tree_show_icons.git` or + `g:nvim_tree_git_hl` to be able to see things in the tree. This will be + changed in the future versions. + + The configurable timeout will kill the current process and so disable the + git integration for the project that takes too long. + The git integration is blocking, so if your timeout is too long (like not in + milliseconds but a few seconds), it will not render anything until the git + process returned the data. + *nvim-tree.view* - |view|: window / buffer setup @@ -296,16 +325,6 @@ width of the window, can be *width_in_columns* or *'width_in_percent%'* where the window will open (default to 'left') - 'left' or 'right' -|g:nvim_tree_gitignore| *g:nvim_tree_gitignore* - -Determines whether to include in g:nvim_tree_ignore -files ignored by git. - -Must be: - 0: not ignored - 1: ignored files from `git ls-files --others --ignored --exclude-standard --directory` - -> |g:nvim_tree_show_icons| *g:nvim_tree_show_icons* Dictionary, if your terminal or font doesn't support certain unicode diff --git a/lua/nvim-tree.lua b/lua/nvim-tree.lua index 16f7103d..bf829958 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -114,7 +114,7 @@ local keypress_funcs = { elseif _config.is_unix then _config.system_open.cmd = 'xdg-open' else - require'nvim-tree.utils'.echo_warning("Cannot open file with system application. Unrecognized platform.") + require'nvim-tree.utils'.warn("Cannot open file with system application. Unrecognized platform.") return end end @@ -173,16 +173,12 @@ function M.on_keypress(mode) if node.link_to and not node.entries then lib.open_file(mode, node.link_to) elseif node.entries ~= nil then - lib.unroll_dir(node) + lib.expand_or_collapse(node) else lib.open_file(mode, node.absolute_path) end end -function M.refresh() - lib.refresh_tree() -end - function M.print_clipboard() fs.print_clipboard() end @@ -227,7 +223,7 @@ function M.on_enter(opts) M.hijack_current_window() end - lib.init(should_open, should_open) + lib.init(should_open) end local function is_file_readable(fname) @@ -242,7 +238,7 @@ local function update_base_dir_with_filepath(filepath, bufnr) local ft = api.nvim_buf_get_option(bufnr, 'filetype') or "" for _, value in pairs(_config.update_focused_file.ignore_list) do - if vim.fn.stridx(filepath, value) ~= -1 or vim.fn.stridx(ft, value) ~= -1 then + if utils.str_find(filepath, value) or utils.str_find(ft, value) then return end end @@ -359,7 +355,7 @@ local function setup_vim_commands() command! NvimTreeClose lua require'nvim-tree'.close() command! NvimTreeToggle lua require'nvim-tree'.toggle(false) command! NvimTreeFocus lua require'nvim-tree'.focus() - command! NvimTreeRefresh lua require'nvim-tree'.refresh() + command! NvimTreeRefresh lua require'nvim-tree.lib'.refresh_tree() command! NvimTreeClipboard lua require'nvim-tree'.print_clipboard() command! NvimTreeFindFile lua require'nvim-tree'.find_file(true) command! NvimTreeFindFileToggle lua require'nvim-tree'.toggle(true) @@ -381,8 +377,8 @@ local function setup_autocommands(opts) """ reset highlights when colorscheme is changed au ColorScheme * lua require'nvim-tree'.reset_highlight() - au BufWritePost * lua require'nvim-tree'.refresh() - au User FugitiveChanged,NeogitStatusRefreshed lua require'nvim-tree'.refresh() + au BufWritePost * lua require'nvim-tree.lib'.refresh_tree() + au User FugitiveChanged,NeogitStatusRefreshed lua require'nvim-tree.lib'.reload_git() ]] if opts.auto_close then @@ -400,6 +396,7 @@ local function setup_autocommands(opts) if opts.update_focused_file.enable then vim.cmd "au BufEnter * lua require'nvim-tree'.find_file(false)" end + vim.cmd "au BufUnload NvimTree lua require'nvim-tree.view'.View.tabpages = {}" vim.cmd "augroup end" end @@ -439,6 +436,11 @@ local DEFAULT_OPTS = { filters = { dotfiles = false, custom_filter = {} + }, + git = { + enable = true, + ignore = true, + timeout = 400, } } @@ -452,7 +454,7 @@ function M.setup(conf) _config.open_on_setup = opts.open_on_setup _config.ignore_ft_on_setup = opts.ignore_ft_on_setup if type(opts.update_to_buf_dir) == "boolean" then - utils.echo_warning("update_to_buf_dir is now a table, see :help nvim-tree.update_to_buf_dir") + utils.warn("update_to_buf_dir is now a table, see :help nvim-tree.update_to_buf_dir") _config.update_to_buf_dir = { enable = opts.update_to_buf_dir, auto_open = opts.update_to_buf_dir, @@ -462,13 +464,14 @@ function M.setup(conf) end if opts.lsp_diagnostics ~= nil then - utils.echo_warning("setup.lsp_diagnostics has been removed, see :help nvim-tree.diagnostics") + utils.warn("setup.lsp_diagnostics has been removed, see :help nvim-tree.diagnostics") end require'nvim-tree.colors'.setup() require'nvim-tree.view'.setup(opts.view or {}) require'nvim-tree.diagnostics'.setup(opts) require'nvim-tree.populate'.setup(opts) + require'nvim-tree.git'.setup(opts) setup_autocommands(opts) setup_vim_commands() diff --git a/lua/nvim-tree/config.lua b/lua/nvim-tree/config.lua index 6fed5e69..e89d9393 100644 --- a/lua/nvim-tree/config.lua +++ b/lua/nvim-tree/config.lua @@ -58,12 +58,6 @@ function M.get_icon_state() } end -function M.use_git() - return M.get_icon_state().show_git_icon - or vim.g.nvim_tree_git_hl == 1 - or vim.g.nvim_tree_gitignore == 1 -end - function M.nvim_tree_callback(callback_name) return string.format(":lua require'nvim-tree'.on_keypress('%s')", callback_name) end diff --git a/lua/nvim-tree/fs.lua b/lua/nvim-tree/fs.lua index 112ec7a6..bded10f5 100644 --- a/lua/nvim-tree/fs.lua +++ b/lua/nvim-tree/fs.lua @@ -34,7 +34,7 @@ local function create_file(file) else luv.fs_close(fd) events._dispatch_file_created(file) - lib.refresh_tree(true) + lib.refresh_tree() focus_file(file) end end)) @@ -98,7 +98,7 @@ function M.create(node) end api.nvim_out_write(ans..' was properly created\n') events._dispatch_folder_created(ans) - lib.refresh_tree(true) + lib.refresh_tree() focus_file(ans) end @@ -113,6 +113,9 @@ local function clear_buffer(absolute_path) api.nvim_set_current_win(winnr) end vim.api.nvim_buf_delete(buf.bufnr, {}) + if buf.windows[1] then + vim.api.nvim_win_close(buf.windows[1], true) + end return end end @@ -239,7 +242,7 @@ local function do_paste(node, action_type, action_fn) end clipboard[action_type] = {} - return lib.refresh_tree(true) + return lib.refresh_tree() end local function add_to_clipboard(node, clip) @@ -276,7 +279,7 @@ function M.remove(node) events._dispatch_file_removed(node.absolute_path) clear_buffer(node.absolute_path) end - lib.refresh_tree(true) + lib.refresh_tree() end end @@ -289,7 +292,13 @@ function M.rename(with_sub) local abs_path = with_sub and node.absolute_path:sub(0, namelen * (-1) -1) or node.absolute_path local new_name = vim.fn.input("Rename " ..node.name.. " to ", abs_path) utils.clear_prompt() - if not new_name or #new_name == 0 then return end + if not new_name or #new_name == 0 then + return + end + if luv.fs_access(new_name, 'R') then + utils.warn("Cannot rename: file already exists") + return + end local success = luv.fs_rename(node.absolute_path, new_name) if not success then @@ -298,7 +307,7 @@ function M.rename(with_sub) api.nvim_out_write(node.absolute_path..' ➜ '..new_name..'\n') rename_loaded_buffers(node.absolute_path, new_name) events._dispatch_node_renamed(abs_path, new_name) - lib.refresh_tree(true) + lib.refresh_tree() end end diff --git a/lua/nvim-tree/git.lua b/lua/nvim-tree/git.lua deleted file mode 100644 index 95642779..00000000 --- a/lua/nvim-tree/git.lua +++ /dev/null @@ -1,168 +0,0 @@ -local utils = require'nvim-tree.utils' -local M = {} - -local roots = {} - ----A map from git roots to a list of ignored paths -local gitignore_map = {} - -local not_git = 'not a git repo' -local is_win = vim.api.nvim_call_function("has", {"win32"}) == 1 - -local function update_root_status(root) - local e_root = vim.fn.shellescape(root) - local untracked = ' -u' - - local cmd = "git -C " .. e_root .. " config --type=bool status.showUntrackedFiles" - if vim.trim(vim.fn.system(cmd)) == 'false' then - untracked = '' - end - - cmd = "git -C " .. e_root .. " status --porcelain=v1 --ignored=matching" .. untracked - local status = vim.fn.systemlist(cmd) - - roots[root] = {} - gitignore_map[root] = {} - - for _, v in pairs(status) do - local head = v:sub(0, 2) - local body = v:sub(4, -1) - if body:match('%->') ~= nil then - body = body:gsub('^.* %-> ', '') - end - - --- Git returns paths with a forward slash wherever you run it, thats why i have to replace it only on windows - if is_win then - body = body:gsub("/", "\\") - end - - roots[root][body] = head - - if head == "!!" then - gitignore_map[root][utils.path_remove_trailing(utils.path_join({root, body}))] = true - end - end -end - -function M.reload_roots() - for root, status in pairs(roots) do - if status ~= not_git then - update_root_status(root) - end - end -end - -local function get_git_root(path) - if roots[path] then - return path, roots[path] - end - - for name, status in pairs(roots) do - if status ~= not_git then - if path:match(utils.path_to_matching_str(name)) then - return name, status - end - end - end -end - -local function create_root(cwd) - local cmd = "git -C " .. vim.fn.shellescape(cwd) .. " rev-parse --show-toplevel" - local git_root = vim.fn.system(cmd) - - if not git_root or #git_root == 0 or git_root:match('fatal') then - roots[cwd] = not_git - return false - end - - if is_win then - git_root = git_root:gsub("/", "\\") - end - - update_root_status(git_root:sub(0, -2)) - return true -end - ----Get the root of the git dir containing the given path or `nil` if it's not a ----git dir. ----@param path string ----@return string|nil -function M.git_root(path) - local git_root, git_status = get_git_root(path) - if not git_root then - if not create_root(path) then - return - end - git_root, git_status = get_git_root(path) - end - - if git_status == not_git then - return - end - - return git_root -end - -function M.update_status(entries, cwd, parent_node, with_redraw) - local git_root, git_status = get_git_root(cwd) - if not git_root then - if not create_root(cwd) then - return - end - git_root, git_status = get_git_root(cwd) - elseif git_status == not_git then - return - end - - if not git_root then - return - end - - if not parent_node then parent_node = {} end - - local matching_cwd = utils.path_to_matching_str( utils.path_add_trailing(git_root) ) - - for _, node in pairs(entries) do - if parent_node.git_status == "!!" then - node.git_status = "!!" - else - local relpath = node.absolute_path:gsub(matching_cwd, '') - if node.entries ~= nil then - relpath = utils.path_add_trailing(relpath) - node.git_status = nil - end - - local status = git_status[relpath] - if status then - node.git_status = status - elseif node.entries ~= nil then - local matcher = '^'..utils.path_to_matching_str(relpath) - for key, entry_status in pairs(git_status) do - if entry_status ~= "!!" and key:match(matcher) then - node.git_status = entry_status - break - end - end - else - node.git_status = nil - end - end - end - if with_redraw then - require'nvim-tree.lib'.redraw() - end -end - ----Check if the given path is ignored by git. ----@param path string Absolute path ----@return boolean -function M.should_gitignore(path) - for _, paths in pairs(gitignore_map) do - if paths[path] == true then - return true - end - end - return false -end - -return M diff --git a/lua/nvim-tree/git/init.lua b/lua/nvim-tree/git/init.lua new file mode 100644 index 00000000..31db0fb3 --- /dev/null +++ b/lua/nvim-tree/git/init.lua @@ -0,0 +1,86 @@ +local git_utils = require'nvim-tree.git.utils' +local Runner = require'nvim-tree.git.runner' + +local M = { + config = nil, + projects = {}, + cwd_to_project_root = {} +} + +function M.reload(callback) + local num_projects = vim.tbl_count(M.projects) + if not M.config.enable or num_projects == 0 then + return callback({}) + end + + local done = 0 + for project_root in pairs(M.projects) do + M.projects[project_root] = {} + Runner.run { + project_root = project_root, + list_untracked = git_utils.should_show_untracked(project_root), + list_ignored = M.config.ignore, + timeout = M.config.timeout, + on_end = function(git_status) + M.projects[project_root] = { + files = git_status, + dirs = git_utils.file_status_to_dir_status(git_status, project_root) + } + done = done + 1 + if done == num_projects then + callback(M.projects) + end + end + } + end +end + +function M.get_project_root(cwd) + if M.cwd_to_project_root[cwd] then + return M.cwd_to_project_root[cwd] + end + + if M.cwd_to_project_root[cwd] == false then + return nil + end + + local project_root = git_utils.get_toplevel(cwd) + return project_root +end + +function M.load_project_status(cwd, callback) + if not M.config.enable then + return callback({}) + end + + local project_root = M.get_project_root(cwd) + if not project_root then + M.cwd_to_project_root[cwd] = false + return callback({}) + end + + local status = M.projects[project_root] + if status then + return callback(status) + end + + Runner.run { + project_root = project_root, + list_untracked = git_utils.should_show_untracked(project_root), + list_ignored = M.config.ignore, + timeout = M.config.timeout, + on_end = function(git_status) + M.projects[project_root] = { + files = git_status, + dirs = git_utils.file_status_to_dir_status(git_status, project_root) + } + callback(M.projects[project_root]) + end + } +end + +function M.setup(opts) + M.config = opts.git +end + +return M diff --git a/lua/nvim-tree/git/runner.lua b/lua/nvim-tree/git/runner.lua new file mode 100644 index 00000000..02f66c49 --- /dev/null +++ b/lua/nvim-tree/git/runner.lua @@ -0,0 +1,99 @@ +local uv = vim.loop +local utils = require'nvim-tree.utils' + +local Runner = {} +Runner.__index = Runner + +function Runner:_parse_status_output(line) + local status = line:sub(1, 2) + -- removing `"` when git is returning special file status containing spaces + local path = line:sub(4, -2):gsub('^"', ''):gsub('"$', '') + if #status > 0 and #path > 0 then + self.output[utils.path_remove_trailing(utils.path_join({self.project_root,path}))] = status + end + return #line +end + +function Runner:_handle_incoming_data(prev_output, incoming) + if incoming and utils.str_find(incoming, '\n') then + local prev = prev_output..incoming + local i = 1 + for line in prev:gmatch('[^\n]*\n') do + i = i + self:_parse_status_output(line) + end + + return prev:sub(i, -1) + end + + if incoming then + return prev_output..incoming + end + + for line in prev_output:gmatch('[^\n]*\n') do + self._parse_status_output(line) + end + + return nil +end + +function Runner:_getopts(stdout_handle) + local untracked = self.list_untracked and '-u' or nil + local ignored = self.list_ignored and '--ignored=matching' or '--ignored=no' + return { + args = {"status", "--porcelain=v1", ignored, untracked}, + cwd = self.project_root, + stdio = { nil, stdout_handle, nil }, + } +end + +function Runner:_run_git_job() + local handle, pid + local stdout = uv.new_pipe(false) + local timer = uv.new_timer() + + local function on_finish(output) + if timer:is_closing() or stdout:is_closing() or handle:is_closing() then + return + end + timer:stop() + timer:close() + stdout:read_stop() + stdout:close() + handle:close() + pcall(uv.kill, pid) + + self.on_end(output or self.output) + end + + handle, pid = uv.spawn( + "git", + self:_getopts(stdout), + vim.schedule_wrap(function() on_finish() end) + ) + + timer:start(self.timeout, 0, vim.schedule_wrap(function() on_finish({}) end)) + + local output_leftover = '' + local function manage_output(err, data) + if err then return end + output_leftover = self:_handle_incoming_data(output_leftover, data) + end + + uv.read_start(stdout, vim.schedule_wrap(manage_output)) +end + +-- This module runs a git process, which will be killed if it takes more than timeout which defaults to 400ms +function Runner.run(opts) + local self = setmetatable({ + project_root = opts.project_root, + list_untracked = opts.list_untracked, + list_ignored = opts.list_ignored, + timeout = opts.timeout or 400, + output = {}, + on_end = opts.on_end, + }, Runner) + + self:_run_git_job() +end + +return Runner diff --git a/lua/nvim-tree/git/utils.lua b/lua/nvim-tree/git/utils.lua new file mode 100644 index 00000000..cc4368ec --- /dev/null +++ b/lua/nvim-tree/git/utils.lua @@ -0,0 +1,54 @@ +local M = {} + +function M.get_toplevel(cwd) + local cmd = "git -C " .. vim.fn.shellescape(cwd) .. " rev-parse --show-toplevel" + local toplevel = vim.fn.system(cmd) + + if not toplevel or #toplevel == 0 or toplevel:match('fatal') then + return nil + end + + -- git always returns path with forward slashes + if vim.fn.has('win32') == 1 then + toplevel = toplevel:gsub("/", "\\") + end + + -- remove newline + return toplevel:sub(0, -2) +end + +local untracked = {} + +function M.should_show_untracked(cwd) + if untracked[cwd] ~= nil then + return untracked[cwd] + end + + local cmd = "git -C "..cwd.." config --type=bool status.showUntrackedFiles" + local has_untracked = vim.fn.system(cmd) + untracked[cwd] = vim.trim(has_untracked) ~= 'false' + return untracked[cwd] +end + +function M.file_status_to_dir_status(status, cwd) + local dirs = {} + for p, s in pairs(status) do + if s ~= '!!' then + local modified = vim.fn.fnamemodify(p, ':h') + dirs[modified] = 'dirty' + end + end + + for dirname, _ in pairs(dirs) do + local modified = dirname + while modified ~= cwd and modified ~= '/' do + modified = vim.fn.fnamemodify(modified, ':h') + dirs[modified] = 'dirty' + end + end + + return dirs +end + +return M + diff --git a/lua/nvim-tree/lib.lua b/lua/nvim-tree/lib.lua index b2cbdfa2..4dafed62 100644 --- a/lua/nvim-tree/lib.lua +++ b/lua/nvim-tree/lib.lua @@ -3,12 +3,12 @@ local luv = vim.loop local renderer = require'nvim-tree.renderer' local config = require'nvim-tree.config' -local git = require'nvim-tree.git' local diagnostics = require'nvim-tree.diagnostics' local pops = require'nvim-tree.populate' local utils = require'nvim-tree.utils' local view = require'nvim-tree.view' local events = require'nvim-tree.events' +local git = require'nvim-tree.git' local populate = pops.populate local refresh_entries = pops.refresh_entries @@ -19,33 +19,25 @@ local M = {} M.Tree = { entries = {}, cwd = nil, - loaded = false, target_winid = nil, } -function M.init(with_open, with_reload) - M.Tree.entries = {} - if not M.Tree.cwd then - M.Tree.cwd = luv.cwd() - end - if config.use_git() then - git.git_root(M.Tree.cwd) - end - populate(M.Tree.entries, M.Tree.cwd) +local function load_children(cwd, children, parent) + git.load_project_status(cwd, function(git_statuses) + populate(children, cwd, parent, git_statuses) + M.redraw() + end) +end - local stat = luv.fs_stat(M.Tree.cwd) - M.Tree.last_modified = stat.mtime.sec +function M.init(with_open, foldername) + M.Tree.entries = {} + M.Tree.cwd = foldername or luv.cwd() if with_open then M.open() - elseif view.win_open() then - M.refresh_tree() end - if with_reload then - renderer.draw(M.Tree, true) - M.Tree.loaded = true - end + load_children(M.Tree.cwd, M.Tree.entries) if not first_init_done then events._dispatch_ready() @@ -85,7 +77,7 @@ local function get_line_from_node(node, find_parent) local function iter(entries, recursive) for _, entry in ipairs(entries) do local n = M.get_last_group_node(entry) - if node_path:match('^'..n.match_path..'$') ~= nil then + if node_path == n.absolute_path then return line, entry end @@ -132,73 +124,75 @@ function M.get_last_group_node(node) return next end -function M.unroll_dir(node) +function M.expand_or_collapse(node) node.open = not node.open if node.has_children then node.has_children = false end - if #node.entries > 0 then - renderer.draw(M.Tree, true) + if #node.entries == 0 then + load_children( + node.link_to or node.absolute_path, + node.entries, + node + ) else - if config.use_git() then - git.git_root(node.absolute_path) - end - populate(node.entries, node.link_to or node.absolute_path, node) - - renderer.draw(M.Tree, true) + M.redraw() end diagnostics.update() end -local function refresh_git(node) - if not node then node = M.Tree end - git.update_status(node.entries, node.absolute_path or node.cwd, node, false) - for _, entry in pairs(node.entries) do - if entry.entries and #entry.entries > 0 then - refresh_git(entry) - end - end -end - --- TODO update only entries where directory has changed -local function refresh_nodes(node) - refresh_entries(node.entries, node.absolute_path or node.cwd, node) +local function refresh_nodes(node, projects) + local project_root = git.get_project_root(node.absolute_path or node.cwd) + refresh_entries(node.entries, node.absolute_path or node.cwd, node, projects[project_root] or {}) for _, entry in ipairs(node.entries) do if entry.entries and entry.open then - refresh_nodes(entry) + refresh_nodes(entry, projects) end end end --- this variable is used to bufferize the refresh actions --- so only one happens every second at most -local refreshing = false - -function M.refresh_tree(disable_clock) - if not M.Tree.cwd or (not disable_clock and refreshing) or vim.v.exiting ~= vim.NIL then +local event_running = false +function M.refresh_tree() + if event_running or not M.Tree.cwd or vim.v.exiting ~= vim.NIL then return end - refreshing = true + event_running = true - refresh_nodes(M.Tree) - - local use_git = config.use_git() - if use_git then - vim.schedule(function() - git.reload_roots() - refresh_git(M.Tree) + git.reload(function(projects) + refresh_nodes(M.Tree, projects) + if view.win_open() then M.redraw() - end) + end + diagnostics.update() + event_running = false + end) +end + +local function reload_node_status(parent_node, projects) + local project_root = git.get_project_root(parent_node.absolute_path or parent_node.cwd) + local status = projects[project_root] or {} + for _, node in ipairs(parent_node.entries) do + if node.entries then + node.git_status = status.dirs and status.dirs[node.absolute_path] + else + node.git_status = status.files and status.files[node.absolute_path] + end + if node.entries and #node.entries > 0 then + reload_node_status(node, projects) + end end +end - vim.schedule(diagnostics.update) - - if view.win_open() then - renderer.draw(M.Tree, true) - else - M.Tree.loaded = false +function M.reload_git() + if not git.config.enable or event_running then + return end + event_running = true - vim.defer_fn(function() refreshing = false end, vim.g.nvim_tree_refresh_wait or 1000) + git.reload(function(projects) + reload_node_status(M.Tree, projects) + M.redraw() + event_running = false + end) end function M.set_index_and_redraw(fname) @@ -209,40 +203,46 @@ function M.set_index_and_redraw(fname) else i = 1 end - local reload = false - local function iter(entries) - for _, entry in ipairs(entries) do + local tree_altered = false + + local function iterate_nodes(nodes) + for _, node in ipairs(nodes) do i = i + 1 - if entry.absolute_path == fname then + if node.absolute_path == fname then return i end - if fname:match(entry.match_path..utils.path_separator) ~= nil then - if #entry.entries == 0 then - reload = true - populate(entry.entries, entry.absolute_path, entry) + local path_matches = utils.str_find(fname, node.absolute_path..utils.path_separator) + if path_matches then + if #node.entries == 0 then + node.open = true + populate(node.entries, node.absolute_path, node, {}) + git.load_project_status(node.absolute_path, function(status) + if status.dirs or status.files then + reload_node_status(node, git.projects) + M.redraw() + end + end) end - if entry.open == false then - reload = true - entry.open = true + if node.open == false then + node.open = true + tree_altered = true end - if iter(entry.entries) ~= nil then + if iterate_nodes(node.entries) ~= nil then return i end - elseif entry.open == true then - iter(entry.entries) + elseif node.open == true then + iterate_nodes(node.entries) end end end - local index = iter(M.Tree.entries) - if not view.win_open() then - M.Tree.loaded = false - return + local index = iterate_nodes(M.Tree.entries) + if tree_altered then + M.redraw() end - renderer.draw(M.Tree, reload) - if index then + if index and view.win_open() then view.set_cursor({index, 0}) end end @@ -405,8 +405,6 @@ function M.open_file(mode, filename) if vim.g.nvim_tree_quit_on_open == 1 then view.close() end - - renderer.draw(M.Tree, true) end function M.open_file_in_tab(filename) @@ -467,8 +465,7 @@ function M.change_dir(name) end vim.cmd('lcd '..vim.fn.fnameescape(foldername)) - M.Tree.cwd = foldername - M.init(false, true) + M.init(false, foldername) end function M.set_target_win() @@ -486,18 +483,19 @@ function M.open() M.set_target_win() local cwd = vim.fn.getcwd() - view.open() + local should_redraw = view.open() local respect_buf_cwd = vim.g.nvim_tree_respect_buf_cwd or 0 - if M.Tree.loaded and (respect_buf_cwd == 1 and cwd ~= M.Tree.cwd) then + if respect_buf_cwd == 1 and cwd ~= M.Tree.cwd then M.change_dir(cwd) end - renderer.draw(M.Tree, not M.Tree.loaded) - M.Tree.loaded = true + if should_redraw then + M.redraw() + end end function M.sibling(node, direction) - if not direction then return end + if node.name == '..' or not direction then return end local iter = get_line_from_node(node, true) local node_path = node.absolute_path @@ -507,7 +505,7 @@ function M.sibling(node, direction) -- Check if current node is already at root entries for index, entry in ipairs(M.Tree.entries) do - if node_path:match('^'..entry.match_path..'$') ~= nil then + if node_path == entry.absolute_path then line = index end end @@ -534,7 +532,6 @@ function M.sibling(node, direction) line, _ = get_line_from_node(target_node)(M.Tree.entries, true) view.set_cursor({line, 0}) - renderer.draw(M.Tree, true) end function M.close_node(node) @@ -543,11 +540,14 @@ end function M.parent_node(node, should_close) if node.name == '..' then return end + should_close = should_close or false + local altered_tree = false local iter = get_line_from_node(node, true) if node.open == true and should_close then node.open = false + altered_tree = true else local line, parent = iter(M.Tree.entries, true) if parent == nil then @@ -555,9 +555,12 @@ function M.parent_node(node, should_close) elseif should_close then parent.open = false end - api.nvim_win_set_cursor(view.get_winnr(), {line, 0}) + view.set_cursor({line, 0}) + end + + if altered_tree then + M.redraw() end - renderer.draw(M.Tree, true) end function M.toggle_ignored() diff --git a/lua/nvim-tree/populate.lua b/lua/nvim-tree/populate.lua index 7f360137..f1bd312b 100644 --- a/lua/nvim-tree/populate.lua +++ b/lua/nvim-tree/populate.lua @@ -1,17 +1,13 @@ -local config = require'nvim-tree.config' -local git = require'nvim-tree.git' - local api = vim.api local luv = vim.loop +local utils = require'nvim-tree.utils' + local M = { ignore_list = {} } -local utils = require'nvim-tree.utils' -local path_to_matching_str = utils.path_to_matching_str - -local function dir_new(cwd, name) +local function dir_new(cwd, name, status, parent_ignored) local absolute_path = utils.path_join({cwd, name}) local stat = luv.fs_stat(absolute_path) local handle = luv.fs_scandir(absolute_path) @@ -28,16 +24,15 @@ local function dir_new(cwd, name) absolute_path = absolute_path, -- TODO: last modified could also involve atime and ctime last_modified = last_modified, - match_name = path_to_matching_str(name), - match_path = path_to_matching_str(absolute_path), open = false, group_next = nil, -- If node is grouped, this points to the next child dir/link node has_children = has_children, - entries = {} + entries = {}, + git_status = parent_ignored and '!!' or (status.dirs and status.dirs[absolute_path]) or (status.files and status.files[absolute_path]), } end -local function file_new(cwd, name) +local function file_new(cwd, name, status, parent_ignored) local absolute_path = utils.path_join({cwd, name}) local is_exec = luv.fs_access(absolute_path, 'X') return { @@ -45,8 +40,7 @@ local function file_new(cwd, name) absolute_path = absolute_path, executable = is_exec, extension = string.match(name, ".?[^.]+%.(.*)") or "", - match_name = path_to_matching_str(name), - match_path = path_to_matching_str(absolute_path), + git_status = parent_ignored and '!!' or status.files and status.files[absolute_path], } end @@ -55,8 +49,7 @@ end -- links (for instance libr2.so in /usr/lib) and thus even with a C program realpath fails -- when it has no real reason to. Maybe there is a reason, but errno is definitely wrong. -- So we need to check for link_to ~= nil when adding new links to the main tree -local function link_new(cwd, name) - +local function link_new(cwd, name, status, parent_ignored) --- I dont know if this is needed, because in my understanding, there isnt hard links in windows, but just to be sure i changed it. local absolute_path = utils.path_join({ cwd, name }) local link_to = luv.fs_realpath(absolute_path) @@ -80,8 +73,7 @@ local function link_new(cwd, name) open = open, group_next = nil, -- If node is grouped, this points to the next child dir/link node entries = entries, - match_name = path_to_matching_str(name), - match_path = path_to_matching_str(absolute_path), + git_status = parent_ignored and '!!' or status.files and status.files[absolute_path], } end @@ -105,6 +97,9 @@ local function should_group(cwd, dirs, files, links) end local function node_comparator(a, b) + if not (a and b) then + return true + end if a.entries and not b.entries then return true elseif not a.entries and b.entries then @@ -130,12 +125,6 @@ local function should_ignore(path) return false end - if vim.g.nvim_tree_gitignore == 1 then - if git.should_gitignore(path) then - return true - end - end - local relpath = utils.path_relative(path, vim.loop.cwd()) if M.ignore_list[relpath] == true or M.ignore_list[basename] == true then return true @@ -151,7 +140,11 @@ local function should_ignore(path) return false end -function M.refresh_entries(entries, cwd, parent_node) +local function should_ignore_git(path, status) + return M.config.filter_ignored and (status and status[path] == '!!') +end + +function M.refresh_entries(entries, cwd, parent_node, status) local handle = luv.fs_scandir(cwd) if type(handle) == 'string' then api.nvim_err_writeln(handle) @@ -162,6 +155,9 @@ function M.refresh_entries(entries, cwd, parent_node) local cached_entries = {} local entries_idx = {} for i, node in ipairs(entries) do + node.git_status = (parent_node and parent_node.git_status == '!!' and '!!') + or (status.files and status.files[node.absolute_path]) + or (status.dirs and status.dirs[node.absolute_path]) cached_entries[i] = node.name entries_idx[node.name] = i named_entries[node.name] = node @@ -179,7 +175,7 @@ function M.refresh_entries(entries, cwd, parent_node) num_new_entries = num_new_entries + 1 local abs = utils.path_join({cwd, name}) - if not should_ignore(abs) then + if not should_ignore(abs) and not should_ignore_git(abs, status.files) then if not t then local stat = luv.fs_stat(abs) t = stat and stat.type @@ -208,7 +204,7 @@ function M.refresh_entries(entries, cwd, parent_node) parent_node.group_next = nil named_entries[next_node.name] = next_node else - M.refresh_entries(entries, next_node.absolute_path, next_node) + M.refresh_entries(entries, next_node.absolute_path, next_node, status) return end end @@ -245,7 +241,7 @@ function M.refresh_entries(entries, cwd, parent_node) for _, name in ipairs(e.entries) do change_prev = true if not named_entries[name] then - local n = e.fn(cwd, name) + local n = e.fn(cwd, name, status) if e.check(n.link_to, n.absolute_path) then new_nodes_added = true idx = 1 @@ -274,7 +270,7 @@ function M.refresh_entries(entries, cwd, parent_node) end end -function M.populate(entries, cwd, parent_node) +function M.populate(entries, cwd, parent_node, status) local handle = luv.fs_scandir(cwd) if type(handle) == 'string' then api.nvim_err_writeln(handle) @@ -290,7 +286,7 @@ function M.populate(entries, cwd, parent_node) if not name then break end local abs = utils.path_join({cwd, name}) - if not should_ignore(abs) then + if not should_ignore(abs) and not should_ignore_git(abs, status.files) then if not t then local stat = luv.fs_stat(abs) t = stat and stat.type @@ -306,52 +302,42 @@ function M.populate(entries, cwd, parent_node) end end - -- Create Nodes -- - + local parent_node_ignored = parent_node and parent_node.git_status == '!!' -- Group empty dirs if parent_node and vim.g.nvim_tree_group_empty == 1 then if should_group(cwd, dirs, files, links) then local child_node - if dirs[1] then child_node = dir_new(cwd, dirs[1]) end - if links[1] then child_node = link_new(cwd, links[1]) end + if dirs[1] then child_node = dir_new(cwd, dirs[1], status, parent_node_ignored) end + if links[1] then child_node = link_new(cwd, links[1], status, parent_node_ignored) end if luv.fs_access(child_node.absolute_path, 'R') then parent_node.group_next = child_node child_node.git_status = parent_node.git_status - M.populate(entries, child_node.absolute_path, child_node) + M.populate(entries, child_node.absolute_path, child_node, status) return end end end for _, dirname in ipairs(dirs) do - local dir = dir_new(cwd, dirname) + local dir = dir_new(cwd, dirname, status, parent_node_ignored) if luv.fs_access(dir.absolute_path, 'R') then table.insert(entries, dir) end end for _, linkname in ipairs(links) do - local link = link_new(cwd, linkname) + local link = link_new(cwd, linkname, status, parent_node_ignored) if link.link_to ~= nil then table.insert(entries, link) end end for _, filename in ipairs(files) do - local file = file_new(cwd, filename) + local file = file_new(cwd, filename, status, parent_node_ignored) table.insert(entries, file) end utils.merge_sort(entries, node_comparator) - - local icon_config = config.get_icon_state() - if (not icon_config.show_git_icon) and vim.g.nvim_tree_git_hl ~= 1 then - return - end - - if config.use_git() then - vim.schedule(function() git.update_status(entries, cwd, parent_node, true) end) - end end function M.setup(opts) diff --git a/lua/nvim-tree/renderer/init.lua b/lua/nvim-tree/renderer/init.lua index 80c0cc29..7e0af5fb 100644 --- a/lua/nvim-tree/renderer/init.lua +++ b/lua/nvim-tree/renderer/init.lua @@ -163,7 +163,7 @@ if vim.g.nvim_tree_git_hl == 1 then local icons = git_hl[git_status] if icons == nil then - utils.echo_warning('Unrecognized git state "'..git_status..'". Please open up an issue on https://github.com/kyazdani42/nvim-tree.lua/issues with this message.') + utils.warn('Unrecognized git state "'..git_status..'". Please open up an issue on https://github.com/kyazdani42/nvim-tree.lua/issues with this message.') icons = git_hl.dirty end @@ -239,7 +239,7 @@ if icon_state.show_git_icon then local icons = git_icon_state[git_status] if not icons then if vim.g.nvim_tree_git_hl ~= 1 then - utils.echo_warning('Unrecognized git state "'..git_status..'". Please open up an issue on https://github.com/kyazdani42/nvim-tree.lua/issues with this message.') + utils.warn('Unrecognized git state "'..git_status..'". Please open up an issue on https://github.com/kyazdani42/nvim-tree.lua/issues with this message.') end icons = git_icon_state.dirty end diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua index f5abfe3e..0115f56e 100644 --- a/lua/nvim-tree/utils.lua +++ b/lua/nvim-tree/utils.lua @@ -6,12 +6,16 @@ function M.path_to_matching_str(path) return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)'):gsub('(%_)', '(%%_)') end -function M.echo_warning(msg) +function M.warn(msg) api.nvim_command('echohl WarningMsg') api.nvim_command("echom '[NvimTree] "..msg:gsub("'", "''").."'") api.nvim_command('echohl None') end +function M.str_find(haystack, needle) + return vim.fn.stridx(haystack, needle) ~= -1 +end + function M.read_file(path) local fd = uv.fs_open(path, "r", 438) if not fd then return '' end diff --git a/lua/nvim-tree/view.lua b/lua/nvim-tree/view.lua index a5ae1e1c..174dfaa0 100644 --- a/lua/nvim-tree/view.lua +++ b/lua/nvim-tree/view.lua @@ -304,7 +304,9 @@ local function is_buf_valid(bufnr) end function M.open(options) + local should_redraw = false if not is_buf_valid(M.View.bufnr) then + should_redraw = true create_buffer() end @@ -322,6 +324,7 @@ function M.open(options) if not opts.focus_tree then vim.cmd("wincmd p") end + return should_redraw end local function get_existing_buffers() diff --git a/plugin/nvim-tree-startup.lua b/plugin/nvim-tree-startup.lua index b1f660c8..8af8aa1d 100644 --- a/plugin/nvim-tree-startup.lua +++ b/plugin/nvim-tree-startup.lua @@ -24,7 +24,8 @@ local out_config = { "nvim_tree_disable_keybindings", "nvim_tree_disable_default_keybindings", "nvim_tree_hide_dotfiles", - "nvim_tree_ignore" + "nvim_tree_ignore", + "nvim_tree_gitignore" } local x = vim.tbl_filter(function(v) @@ -33,5 +34,5 @@ end, out_config) if #x > 0 then local msg = "Following options were moved to setup, see git.io/JPhyt: " - require'nvim-tree.utils'.echo_warning(msg..table.concat(x, ", ")) + require'nvim-tree.utils'.warn(msg..table.concat(x, ", ")) end