diff --git a/README.md b/README.md index e776b504184..cc65a9bc1c6 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Install with [vim-plug](https://github.com/junegunn/vim-plug): ```vim " requires Plug 'kyazdani42/nvim-web-devicons' " for file icons +Plug 'tami5/sqlite.lua' " for git integration Plug 'kyazdani42/nvim-tree.lua' ``` @@ -23,7 +24,10 @@ 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 + 'tami5/sqlite.lua', -- optional, for git integration + }, config = function() require'nvim-tree'.setup {} end } ``` @@ -89,6 +93,17 @@ require'nvim-tree'.setup { args = {} }, + -- git integration (:help nvim-tree.git) + git = { + -- enable the module + enable = true, + -- enable gitignore filtering on files + ignore = true, + -- kills the git integration after this if it takes too long to complete + timeout = 5000, + }, + + -- window/buffer configurations (:help nvim-tree.view) view = { -- width of the window, can be either a number (columns) or a string in `%`, for left or right side placement width = 30, @@ -111,7 +126,6 @@ require'nvim-tree'.setup { ```vim let g:nvim_tree_ignore = [ '.git', 'node_modules', '.cache' ] "empty by default -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_hide_dotfiles = 1 "0 by default, this option hides files and folders starting with a dot `.` diff --git a/doc/nvim-tree-lua.txt b/doc/nvim-tree-lua.txt index 277b5c3ac76..3716033b95d 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, @@ -227,6 +231,34 @@ 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: `5000` (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 git integration is based on `tami5/sqlite.lua`, and is asynchronous in + order to avoid blocking the UI when parsing big amounts of statuses. + The database is opened on startup and destroyed when neovim exits. + Multiple instances of neovim will spawn multiple databases to avoid + collisions. + + The configurable timeout will kill the current process and + the git integration if its taking too long. + *nvim-tree.view* - |view|: window / buffer setup @@ -281,17 +313,8 @@ An array of strings that the tree won't load and display. useful to hide large data/cache folders. > example: let g:nvim_tree_ignore = [ '.git', 'node_modules' ] +< -|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 e9c0894b930..d63b82c1f92 100644 --- a/lua/nvim-tree.lua +++ b/lua/nvim-tree.lua @@ -179,10 +179,6 @@ function M.on_keypress(mode) 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 @@ -358,7 +354,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) @@ -380,8 +376,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 @@ -399,6 +395,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 @@ -434,6 +431,11 @@ local DEFAULT_OPTS = { error = "", } }, + git = { + enable = true, + ignore = true, + timeout = 400, + } } function M.setup(conf) @@ -462,6 +464,7 @@ function M.setup(conf) require'nvim-tree.colors'.setup() require'nvim-tree.view'.setup(opts.view or {}) require'nvim-tree.diagnostics'.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 6fed5e69627..e89d9393bb6 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 112ec7a6f8f..ee469a35f9e 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 @@ -239,7 +239,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 +276,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 @@ -298,7 +298,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 95642779be9..00000000000 --- 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 00000000000..31db0fb3362 --- /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 00000000000..02f66c4972b --- /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 00000000000..cc4368ec4c3 --- /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 4413a7eabce..0e06a67c002 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 @@ -134,70 +126,70 @@ end function M.unroll_dir(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 +function M.refresh_tree() + if not M.Tree.cwd or vim.v.exiting ~= vim.NIL then return end - refreshing = 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 + end + diagnostics.update() + end) +end - vim.schedule(diagnostics.update) +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 - if view.win_open() then - renderer.draw(M.Tree, true) - else - M.Tree.loaded = false +local running_reload = false +function M.reload_git() + if not git.config.enable or running_reload then + return end + running_reload = true - vim.defer_fn(function() refreshing = false end, vim.g.nvim_tree_refresh_wait or 1000) + git.reload(function(projects) + running_reload = false + reload_node_status(M.Tree, projects) + M.redraw() + end) end function M.set_index_and_redraw(fname) @@ -207,40 +199,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 @@ -403,8 +401,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) @@ -465,8 +461,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() @@ -484,18 +479,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 @@ -505,7 +501,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 @@ -532,7 +528,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) @@ -541,11 +536,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 @@ -553,9 +551,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 28377965674..783251e410e 100644 --- a/lua/nvim-tree/populate.lua +++ b/lua/nvim-tree/populate.lua @@ -1,19 +1,14 @@ -local config = require'nvim-tree.config' -local git = require'nvim-tree.git' -local icon_config = config.get_icon_state() - local api = vim.api local luv = vim.loop +local utils = require'nvim-tree.utils' + local M = { show_ignored = false, show_dotfiles = vim.g.nvim_tree_hide_dotfiles ~= 1, } -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) local absolute_path = utils.path_join({cwd, name}) local stat = luv.fs_stat(absolute_path) local handle = luv.fs_scandir(absolute_path) @@ -30,16 +25,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 = (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) local absolute_path = utils.path_join({cwd, name}) local is_exec = luv.fs_access(absolute_path, 'X') return { @@ -47,8 +41,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 = status.files and status.files[absolute_path], } end @@ -57,8 +50,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) --- 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) @@ -82,8 +74,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 = status.files and status.files[absolute_path], } end @@ -133,10 +124,6 @@ local function gen_ignore_check(cwd) local basename = utils.path_basename(path) if not M.show_ignored then - if vim.g.nvim_tree_gitignore == 1 then - if git.should_gitignore(path) then return true end - end - local relpath = utils.path_relative(path, cwd) if ignore_list[relpath] == true or ignore_list[basename] == true then return true @@ -158,7 +145,11 @@ end local should_ignore = gen_ignore_check() -function M.refresh_entries(entries, cwd, parent_node) +local function should_ignore_git(path, status) + return not M.show_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) @@ -169,6 +160,8 @@ function M.refresh_entries(entries, cwd, parent_node) local cached_entries = {} local entries_idx = {} for i, node in ipairs(entries) do + node.git_status = (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 @@ -186,7 +179,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 @@ -215,7 +208,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 @@ -252,7 +245,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 @@ -281,7 +274,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) @@ -297,7 +290,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 @@ -319,45 +312,37 @@ function M.populate(entries, cwd, parent_node) 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) end + if links[1] then child_node = link_new(cwd, links[1], status) 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) 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) 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) table.insert(entries, file) end utils.merge_sort(entries, node_comparator) - - 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 return M diff --git a/lua/nvim-tree/utils.lua b/lua/nvim-tree/utils.lua index f5abfe3e779..3c048d2a420 100644 --- a/lua/nvim-tree/utils.lua +++ b/lua/nvim-tree/utils.lua @@ -12,6 +12,10 @@ function M.echo_warning(msg) 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 5e61d047ea3..f089be7c0b1 100644 --- a/lua/nvim-tree/view.lua +++ b/lua/nvim-tree/view.lua @@ -302,7 +302,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 @@ -320,6 +322,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 acbf0bc58da..b70c97c8083 100644 --- a/plugin/nvim-tree-startup.lua +++ b/plugin/nvim-tree-startup.lua @@ -23,6 +23,7 @@ local out_config = { "nvim_tree_bindings", "nvim_tree_disable_keybindings", "nvim_tree_disable_default_keybindings", + "nvim_tree_gitignore" } local x = vim.tbl_filter(function(v)