perf(#3231): decrease setup time: optimise setup and defer requires until tree open (#3308)

* perf(#3257): remove devicons setup

* perf(#3257): remove padding setup

* perf(#3257): remove appearance and log setup

* perf(#3257): remove appearance setup

* perf(#3257): remove rename-file setup

* perf(#3257): remove devicons setup

* perf(#3257): remove appearance setup

* perf(#3257): inline require legacy notify

* perf(#3257): inline require events notify

* perf(#3257): inline require log notify

* perf(#3253): extract change-root

* perf(#3253): extract open_on_directory

* perf(#3253): extract tab_enter

* perf(#3253): move requires inline

* perf(#3253): extract au find-file, view

* perf(#3253): extract manage_netrw

* perf(#3253): extract purge_all_state and document

* perf(#3253): move full-name setup_autocommands to main

* perf(#3253): unextract au find-file for laziness

* perf(#3253): short-circuit open_on_directory au

* perf(#3253): extract autocmd

* perf(#3253): move all setup requires inside the function
This commit is contained in:
Alexander Courtis
2026-04-02 13:33:16 +11:00
committed by GitHub
parent 02ccbcd4af
commit 31503ad5d8
13 changed files with 364 additions and 320 deletions

View File

@@ -1,308 +1,58 @@
local api = require("nvim-tree.api")
local log = require("nvim-tree.log")
local view = require("nvim-tree.view")
local utils = require("nvim-tree.utils")
local find_file = require("nvim-tree.actions.tree.find-file")
local change_dir = require("nvim-tree.actions.tree.change-dir")
local full_name = require("nvim-tree.renderer.components.full-name")
local core = require("nvim-tree.core")
local notify = require("nvim-tree.notify")
local config = require("nvim-tree.config")
local M = {}
local M = {
init_root = "",
}
--- Helper function to execute some explorer method safely
---@param fn string # key of explorer
---@param ... any|nil
---@return function|nil
local function explorer_fn(fn, ...)
local explorer = core.get_explorer()
if explorer then
return explorer[fn](explorer, ...)
end
end
--- Update the tree root to a directory or the directory containing
---@param path string relative or absolute
---@param bufnr number|nil
function M.change_root(path, bufnr)
-- skip if current file is in ignore_list
if type(bufnr) == "number" then
local ft
if vim.fn.has("nvim-0.10") == 1 then
ft = vim.api.nvim_get_option_value("filetype", { buf = bufnr }) or ""
else
ft = vim.api.nvim_buf_get_option(bufnr, "filetype") or "" ---@diagnostic disable-line: deprecated
end
for _, value in pairs(config.g.update_focused_file.update_root.ignore_list) do
if utils.str_find(path, value) or utils.str_find(ft, value) then
return
end
end
end
-- don't find inexistent
if vim.fn.filereadable(path) == 0 then
return
end
local cwd = core.get_cwd()
if cwd == nil then
return
end
local vim_cwd = vim.fn.getcwd()
-- test if in vim_cwd
if utils.path_relative(path, vim_cwd) ~= path then
if vim_cwd ~= cwd then
explorer_fn("change_dir", vim_cwd)
end
return
end
-- test if in cwd
if utils.path_relative(path, cwd) ~= path then
return
end
-- otherwise test M.init_root
if config.g.prefer_startup_root and utils.path_relative(path, M.init_root) ~= path then
explorer_fn("change_dir", M.init_root)
return
end
-- otherwise root_dirs
for _, dir in pairs(config.g.root_dirs) do
dir = vim.fn.fnamemodify(dir, ":p")
if utils.path_relative(path, dir) ~= path then
explorer_fn("change_dir", dir)
return
end
end
-- finally fall back to the folder containing the file
explorer_fn("change_dir", vim.fn.fnamemodify(path, ":p:h"))
end
function M.tab_enter()
if view.is_visible({ any_tabpage = true }) then
local bufname = vim.api.nvim_buf_get_name(0)
local ft
if vim.fn.has("nvim-0.10") == 1 then
ft = vim.api.nvim_get_option_value("filetype", { buf = 0 }) or ""
else
ft = vim.api.nvim_buf_get_option(0, "ft") ---@diagnostic disable-line: deprecated
end
for _, filter in ipairs(config.g.tab.sync.ignore) do
if bufname:match(filter) ~= nil or ft:match(filter) ~= nil then
return
end
end
view.open({ focus_tree = false })
local explorer = core.get_explorer()
if explorer then
explorer.renderer:draw()
end
end
end
function M.open_on_directory()
local should_proceed = config.g.hijack_directories.auto_open or view.is_visible()
if not should_proceed then
return
end
local buf = vim.api.nvim_get_current_buf()
local bufname = vim.api.nvim_buf_get_name(buf)
if vim.fn.isdirectory(bufname) ~= 1 then
return
end
local explorer = core.get_explorer()
if not explorer then
core.init(bufname)
end
explorer_fn("force_dirchange", bufname, true, false)
end
local function manage_netrw()
if config.g.hijack_netrw then
vim.cmd("silent! autocmd! FileExplorer *")
vim.cmd("autocmd VimEnter * ++once silent! autocmd! FileExplorer *")
end
if config.g.disable_netrw then
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
end
end
local function setup_autocommands()
local augroup_id = vim.api.nvim_create_augroup("NvimTree", { clear = true })
local function create_nvim_tree_autocmd(name, custom_opts)
local default_opts = { group = augroup_id }
vim.api.nvim_create_autocmd(name, vim.tbl_extend("force", default_opts, custom_opts))
end
-- prevent new opened file from opening in the same window as nvim-tree
create_nvim_tree_autocmd("BufWipeout", {
pattern = "NvimTree_*",
callback = function()
if not utils.is_nvim_tree_buf(0) then
return
end
if config.g.actions.open_file.eject then
view._prevent_buffer_override()
else
view.abandon_current_window()
end
end,
})
if config.g.tab.sync.open then
create_nvim_tree_autocmd("TabEnter", { callback = vim.schedule_wrap(M.tab_enter) })
end
if config.g.sync_root_with_cwd then
create_nvim_tree_autocmd("DirChanged", {
callback = function()
change_dir.fn(vim.loop.cwd())
end,
})
end
if config.g.update_focused_file.enable then
create_nvim_tree_autocmd("BufEnter", {
callback = function(event)
local exclude = config.g.update_focused_file.exclude
if type(exclude) == "function" and exclude(event) then
return
end
utils.debounce("BufEnter:find_file", config.g.view.debounce_delay, function()
find_file.fn()
end)
end,
})
end
if config.g.hijack_directories.enable and (config.g.disable_netrw or config.g.hijack_netrw) then
create_nvim_tree_autocmd({ "BufEnter", "BufNewFile" }, { callback = M.open_on_directory, nested = true })
end
if config.g.view.centralize_selection then
create_nvim_tree_autocmd("BufEnter", {
pattern = "NvimTree_*",
callback = function()
vim.schedule(function()
vim.api.nvim_buf_call(0, function()
local is_term_mode = vim.api.nvim_get_mode().mode == "t"
if is_term_mode then
return
end
vim.cmd([[norm! zz]])
end)
end)
end,
})
end
if config.g.diagnostics.enable then
create_nvim_tree_autocmd("DiagnosticChanged", {
callback = function(ev)
log.line("diagnostics", "DiagnosticChanged")
require("nvim-tree.diagnostics").update_lsp(ev)
end,
})
create_nvim_tree_autocmd("User", {
pattern = "CocDiagnosticChange",
callback = function()
log.line("diagnostics", "CocDiagnosticChange")
require("nvim-tree.diagnostics").update_coc()
end,
})
end
if config.g.view.float.enable and config.g.view.float.quit_on_focus_loss then
create_nvim_tree_autocmd("WinLeave", {
pattern = "NvimTree_*",
callback = function()
if utils.is_nvim_tree_buf(0) then
view.close()
end
end,
})
end
-- Handles event dispatch when tree is closed by `:q`
create_nvim_tree_autocmd("WinClosed", {
pattern = "*",
---@param ev vim.api.keyset.create_autocmd.callback_args
callback = function(ev)
if not vim.api.nvim_buf_is_valid(ev.buf) then
return
end
if vim.api.nvim_get_option_value("filetype", { buf = ev.buf }) == "NvimTree" then
require("nvim-tree.events")._dispatch_on_tree_close()
end
end,
})
-- renderer.full name
full_name.setup_autocommands()
end
function M.purge_all_state()
view.close_all_tabs()
view.abandon_all_windows()
local explorer = core.get_explorer()
if explorer then
require("nvim-tree.git").purge_state()
explorer:destroy()
core.reset_explorer()
end
-- purge orphaned that were not destroyed by their nodes
require("nvim-tree.watcher").purge_watchers()
end
---@param config_user? nvim_tree.config user supplied subset of config
---`require("nvim-tree").setup` must be called once to initialise nvim-tree.
---
---Call again to apply a change in configuration without restarting Nvim.
---
---See :help nvim-tree-setup
---
---@param config_user? nvim_tree.config subset, uses defaults when nil
function M.setup(config_user)
local api = require("nvim-tree.api")
local api_impl = require("nvim-tree.api.impl")
local appearance = require("nvim-tree.appearance")
local autocmd = require("nvim-tree.autocmd")
local config = require("nvim-tree.config")
local log = require("nvim-tree.log")
local view_state = require("nvim-tree.view-state")
-- Nvim version check
if vim.fn.has("nvim-0.9") == 0 then
notify.warn("nvim-tree.lua requires Neovim 0.9 or higher")
require("nvim-tree.notify").warn("nvim-tree.lua requires Neovim 0.9 or higher")
return
end
M.init_root = vim.fn.getcwd()
-- validate and merge with defaults as config.g
config.setup(config_user)
manage_netrw()
-- optionally create the log file
log.start()
-- optionally log the configuration
if log.enabled("config") then
log.line("config", "default config + user")
log.raw("config", "%s\n", vim.inspect(config.g))
end
require("nvim-tree.appearance").highlight()
-- idempotent highlight definition
appearance.highlight()
require("nvim-tree.view-state").initialize()
-- set the initial view state based on config
view_state.initialize()
setup_autocommands()
-- idempotent au (re)definition
autocmd.global()
-- subsequent calls to setup clear all state
if vim.g.NvimTreeSetup == 1 then
-- subsequent calls to setup
M.purge_all_state()
require("nvim-tree.core").purge_all_state()
end
-- hydrate post setup API
api_impl.hydrate_post_setup(api)
vim.g.NvimTreeSetup = 1
vim.api.nvim_exec_autocmds("User", { pattern = "NvimTreeSetup" })
require("nvim-tree.api.impl").hydrate_post_setup(api)
end
vim.g.NvimTreeRequired = 1

View File

@@ -0,0 +1,74 @@
local utils = require("nvim-tree.utils")
local core = require("nvim-tree.core")
local config = require("nvim-tree.config")
local M = {}
--- Update the tree root to a directory or the directory containing
---@param path string relative or absolute
---@param bufnr number|nil
function M.fn(path, bufnr)
-- skip if current file is in ignore_list
if type(bufnr) == "number" then
local ft
if vim.fn.has("nvim-0.10") == 1 then
ft = vim.api.nvim_get_option_value("filetype", { buf = bufnr }) or ""
else
ft = vim.api.nvim_buf_get_option(bufnr, "filetype") or "" ---@diagnostic disable-line: deprecated
end
for _, value in pairs(config.g.update_focused_file.update_root.ignore_list) do
if utils.str_find(path, value) or utils.str_find(ft, value) then
return
end
end
end
-- don't find inexistent
if vim.fn.filereadable(path) == 0 then
return
end
local cwd = core.get_cwd()
if cwd == nil then
return
end
local vim_cwd = vim.fn.getcwd()
local explorer = core.get_explorer()
if not explorer then
return
end
-- test if in vim_cwd
if utils.path_relative(path, vim_cwd) ~= path then
if vim_cwd ~= cwd then
explorer:change_dir(vim_cwd)
end
return
end
-- test if in cwd
if utils.path_relative(path, cwd) ~= path then
return
end
-- otherwise test init_root
if config.g.prefer_startup_root and utils.path_relative(path, config.init_root) ~= path then
explorer:change_dir(config.init_root)
return
end
-- otherwise root_dirs
for _, dir in pairs(config.g.root_dirs) do
dir = vim.fn.fnamemodify(dir, ":p")
if utils.path_relative(path, dir) ~= path then
explorer:change_dir(dir)
return
end
end
-- finally fall back to the folder containing the file
explorer:change_dir(vim.fn.fnamemodify(path, ":p:h"))
end
return M

View File

@@ -3,6 +3,7 @@ local lib = require("nvim-tree.lib")
local view = require("nvim-tree.view")
local config = require("nvim-tree.config")
local finders_find_file = require("nvim-tree.actions.finders.find-file")
local change_root = require("nvim-tree.actions.tree.change-root")
local M = {}
@@ -58,7 +59,7 @@ function M.fn(opts)
-- update root
if opts.update_root or config.g.update_focused_file.update_root.enable then
require("nvim-tree").change_root(path, bufnr)
change_root.fn(path, bufnr)
end
-- find

View File

@@ -1,7 +1,9 @@
local lib = require("nvim-tree.lib")
local view = require("nvim-tree.view")
local core = require("nvim-tree.core")
local config = require("nvim-tree.config")
local finders_find_file = require("nvim-tree.actions.finders.find-file")
local change_root = require("nvim-tree.actions.tree.change-root")
local M = {}
@@ -41,7 +43,7 @@ function M.fn(opts)
if config.g.update_focused_file.enable or opts.find_file then
-- update root
if opts.update_root then
require("nvim-tree").change_root(previous_path, previous_buf)
change_root.fn(previous_path, previous_buf)
end
-- find
@@ -49,4 +51,47 @@ function M.fn(opts)
end
end
---@param dirname string absolute directory path
function M.open_on_directory(dirname)
local should_proceed = config.g.hijack_directories.auto_open or view.is_visible()
if not should_proceed then
return
end
-- instantiate an explorer if there is not one
if not core.get_explorer() then
core.init(dirname)
end
local explorer = core.get_explorer()
if explorer then
explorer:force_dirchange(dirname, true, false)
end
end
function M.tab_enter()
if view.is_visible({ any_tabpage = true }) then
local bufname = vim.api.nvim_buf_get_name(0)
local ft
if vim.fn.has("nvim-0.10") == 1 then
ft = vim.api.nvim_get_option_value("filetype", { buf = 0 }) or ""
else
ft = vim.api.nvim_buf_get_option(0, "ft") ---@diagnostic disable-line: deprecated
end
for _, filter in ipairs(config.g.tab.sync.ignore) do
if bufname:match(filter) ~= nil or ft:match(filter) ~= nil then
return
end
end
view.open({ focus_tree = false })
local explorer = core.get_explorer()
if explorer then
explorer.renderer:draw()
end
end
end
return M

View File

@@ -2,6 +2,7 @@ local lib = require("nvim-tree.lib")
local view = require("nvim-tree.view")
local config = require("nvim-tree.config")
local finders_find_file = require("nvim-tree.actions.finders.find-file")
local change_root = require("nvim-tree.actions.tree.change-root")
local M = {}
@@ -56,7 +57,7 @@ function M.fn(opts, no_focus, cwd, bang)
if config.g.update_focused_file.enable or opts.find_file then
-- update root
if opts.update_root then
require("nvim-tree").change_root(previous_path, previous_buf)
change_root.fn(previous_path, previous_buf)
end
-- find

145
lua/nvim-tree/autocmd.lua Normal file
View File

@@ -0,0 +1,145 @@
local config = require("nvim-tree.config")
local M = {}
---Create all global autocommands after setup.
---Idempotent: removes existing autocommands first.
---For startup performance reasons, all requires must be done inline during the callback.
---Some short circuiting logic is done directly inside the callback to prevent unnecessary requires.
function M.global()
local augroup_id = vim.api.nvim_create_augroup("NvimTree", { clear = true })
vim.api.nvim_create_autocmd("BufWipeout", {
group = augroup_id,
pattern = "NvimTree_*",
callback = function()
require("nvim-tree.view").wipeout()
end,
})
if config.g.tab.sync.open then
vim.api.nvim_create_autocmd("TabEnter", {
group = augroup_id,
callback = vim.schedule_wrap(function()
require("nvim-tree.actions.tree.open").tab_enter()
end)
})
end
if config.g.sync_root_with_cwd then
vim.api.nvim_create_autocmd("DirChanged", {
group = augroup_id,
callback = function()
require("nvim-tree.actions.tree.change-dir").fn(vim.loop.cwd())
end,
})
end
if config.g.update_focused_file.enable then
vim.api.nvim_create_autocmd("BufEnter", {
group = augroup_id,
callback = function(event)
if type(config.g.update_focused_file.exclude) == "function" and config.g.update_focused_file.exclude(event) then
return
end
require("nvim-tree.utils").debounce("BufEnter:find_file", config.g.view.debounce_delay, function()
require("nvim-tree.actions.tree.find-file").fn()
end)
end,
})
end
if config.g.hijack_directories.enable and (config.g.disable_netrw or config.g.hijack_netrw) then
vim.api.nvim_create_autocmd({ "BufEnter", "BufNewFile" }, {
group = augroup_id,
nested = true,
callback = function(data)
local bufname = vim.api.nvim_buf_get_name(data.buf)
if vim.fn.isdirectory(bufname) == 1 then
require("nvim-tree.actions.tree.open").open_on_directory(bufname)
end
end,
})
end
if config.g.view.centralize_selection then
vim.api.nvim_create_autocmd("BufEnter", {
group = augroup_id,
pattern = "NvimTree_*",
callback = vim.schedule_wrap(function()
vim.api.nvim_buf_call(0, function()
local is_term_mode = vim.api.nvim_get_mode().mode == "t"
if is_term_mode then
return
end
vim.cmd([[norm! zz]])
end)
end)
})
end
if config.g.diagnostics.enable then
vim.api.nvim_create_autocmd("DiagnosticChanged", {
group = augroup_id,
callback = function(ev)
require("nvim-tree.diagnostics").update_lsp(ev)
end,
})
vim.api.nvim_create_autocmd("User", {
group = augroup_id,
pattern = "CocDiagnosticChange",
callback = function()
require("nvim-tree.diagnostics").update_coc()
end,
})
end
if config.g.view.float.enable and config.g.view.float.quit_on_focus_loss then
vim.api.nvim_create_autocmd("WinLeave", {
group = augroup_id,
pattern = "NvimTree_*",
callback = function()
if require("nvim-tree.utils").is_nvim_tree_buf(0) then
require("nvim-tree.view").close()
end
end,
})
end
-- Handles event dispatch when tree is closed by `:q`
vim.api.nvim_create_autocmd("WinClosed", {
group = augroup_id,
pattern = "*",
---@param ev vim.api.keyset.create_autocmd.callback_args
callback = function(ev)
if not vim.api.nvim_buf_is_valid(ev.buf) then
return
end
if vim.api.nvim_get_option_value("filetype", { buf = ev.buf }) == "NvimTree" then
require("nvim-tree.events")._dispatch_on_tree_close()
end
end,
})
if config.g.renderer.full_name then
local group = vim.api.nvim_create_augroup("nvim_tree_floating_node", { clear = true })
vim.api.nvim_create_autocmd({ "BufLeave", "CursorMoved" }, {
group = group,
pattern = { "NvimTree_*" },
callback = function()
require("nvim-tree.renderer.components.full-name").hide()
end,
})
vim.api.nvim_create_autocmd({ "CursorMoved" }, {
group = group,
pattern = { "NvimTree_*" },
callback = function()
require("nvim-tree.renderer.components.full-name").show()
end,
})
end
end
return M

View File

@@ -17,7 +17,11 @@ local M = {
---Immutable OS, detected once on first require
---@type table<"unix"|"macos"|"wsl"|"windows", boolean>
os = nil
os = nil,
---Nvim cwd at setup time
---@type string
init_root = "",
}
M.os = {
@@ -528,6 +532,19 @@ local function process_config(g)
end
end
---Hijack and disable netrw if (globally) configured
---@param g nvim_tree.config
local function manage_netrw(g)
if g.hijack_netrw then
vim.cmd("silent! autocmd! FileExplorer *")
vim.cmd("autocmd VimEnter * ++once silent! autocmd! FileExplorer *")
end
if g.disable_netrw then
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
end
end
---Validate user config and migrate legacy.
---Merge with M.d and persist as M.g
---When no user config M.g is set to M.d and M.u is set to nil
@@ -554,6 +571,12 @@ function M.setup(u)
-- process merged config
process_config(M.g)
-- deal with netrw
manage_netrw(M.g)
-- store cwd
M.init_root = vim.fn.getcwd()
end
---Deep clone defaults

View File

@@ -2,6 +2,8 @@ local events = require("nvim-tree.events")
local notify = require("nvim-tree.notify")
local view = require("nvim-tree.view")
local log = require("nvim-tree.log")
local git = require("nvim-tree.git")
local watcher = require("nvim-tree.watcher")
local M = {}
@@ -64,4 +66,16 @@ function M.get_nodes_starting_line()
return offset
end
function M.purge_all_state()
view.close_all_tabs()
view.abandon_all_windows()
if TreeExplorer then
git.purge_state()
TreeExplorer:destroy()
M.reset_explorer()
end
-- purge orphaned that were not destroyed by their nodes
watcher.purge_watchers()
end
return M

View File

@@ -128,6 +128,7 @@ end
---On disabling LSP, a reset event will be sent for all buffers.
---@param ev table standard event with data.diagnostics populated
function M.update_lsp(ev)
log.line("diagnostics", "DiagnosticChanged")
if not config.g.diagnostics.enable or not ev or not ev.data or not ev.data.diagnostics then
return
end
@@ -174,6 +175,7 @@ end
---Fired on CocDiagnosticChanged events:
---debounced retrieval, cache update, version increment and draw
function M.update_coc()
log.line("diagnostics", "CocDiagnosticChange")
if not config.g.diagnostics.enable then
return
end

View File

@@ -771,7 +771,6 @@ function Explorer:cd(global, path)
vim.cmd((global and "cd " or "lcd ") .. vim.fn.fnameescape(path))
end
---@private
---@param foldername string
---@param should_open_view boolean|nil
---@param should_init boolean|nil

View File

@@ -5,10 +5,10 @@ local M = {}
local utils = require("nvim-tree.utils")
local function hide(win)
if win then
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
function M.hide()
if M.popup_win and utils.is_nvim_tree_buf(0) then
if vim.api.nvim_win_is_valid(M.popup_win) then
vim.api.nvim_win_close(M.popup_win, true)
end
end
end
@@ -35,7 +35,11 @@ local function effective_win_width()
return win_width - win_info[1].textoff
end
local function show()
function M.show()
if not utils.is_nvim_tree_buf(0) then
return
end
local line_nr = vim.api.nvim_win_get_cursor(0)[1]
if vim.wo.wrap then
return
@@ -104,31 +108,4 @@ local function show()
end)
end
function M.setup_autocommands()
if not config.g.renderer.full_name then
return
end
local group = vim.api.nvim_create_augroup("nvim_tree_floating_node", { clear = true })
vim.api.nvim_create_autocmd({ "BufLeave", "CursorMoved" }, {
group = group,
pattern = { "NvimTree_*" },
callback = function()
if utils.is_nvim_tree_buf(0) then
hide(M.popup_win)
end
end,
})
vim.api.nvim_create_autocmd({ "CursorMoved" }, {
group = group,
pattern = { "NvimTree_*" },
callback = function()
if utils.is_nvim_tree_buf(0) then
show()
end
end,
})
end
return M

View File

@@ -521,4 +521,17 @@ function M.is_width_determined()
return type(view_state.Active.width) ~= "function"
end
---Called on BufWipeout
---Prevent new opened file from opening in the same window as nvim-tree
function M.wipeout()
if not utils.is_nvim_tree_buf(0) then
return
end
if config.g.actions.open_file.eject then
M._prevent_buffer_override()
else
M.abandon_current_window()
end
end
return M

View File

@@ -238,7 +238,7 @@ M.Watcher = Watcher
function M.disable_watchers(msg)
notify.warn(string.format("Disabling watchers: %s", msg))
config.g.filesystem_watchers.enable = false
require("nvim-tree").purge_all_state()
require("nvim-tree.core").purge_all_state()
end
function M.purge_watchers()