diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 838fa6fcc5..0514f11883 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -316,6 +316,8 @@ EVENTS • |SessionWritePre| event emits just before |:mksession|. • |TextPutPre| and |TextPutPost| are triggered before/after putting text. • |TabMoved| is triggered when tabs are reordered. +• |DirReadPost| is triggered after each |dir| listing is rendered, so it can + be sorted or filtered. See |dir-render|. HIGHLIGHTS diff --git a/runtime/doc/plugins.txt b/runtime/doc/plugins.txt index c40705cd07..a3adf3e3bd 100644 --- a/runtime/doc/plugins.txt +++ b/runtime/doc/plugins.txt @@ -88,6 +88,42 @@ first entry instead of the one it was left on. "wipe" discards the buffer itself, leaving no |alternate-file|; see 'bufhidden'. +Reshaping the listing *dir-render* + + *DirReadPost* +A |User| autocommand fired after each listing is rendered, including reloads. +The buffer is writable for the duration, so a handler can sort or filter it +with ordinary commands, and the cursor is placed afterwards. + +Handlers may reorder or remove lines, but each remaining line must still be an +entry name: |dir-buffer-mappings| resolve the line under the cursor against the +buffer name, so rewriting line text makes open a nonexistent path. + +Sort directories last: >vim + autocmd User DirReadPost silent keeppatterns sort r /\/$/ +< +Hide dot-prefixed entries: >vim + autocmd User DirReadPost silent keeppatterns g/^\./d _ +< +Sort by modification time, newest first: >lua + vim.api.nvim_create_autocmd('User', { + pattern = 'DirReadPost', + callback = function(args) + local dir = vim.api.nvim_buf_get_name(args.buf) + local names = vim.api.nvim_buf_get_lines(args.buf, 0, -1, true) + local mtime = {} --- @type table + for _, name in ipairs(names) do + local stat = vim.uv.fs_stat(vim.fs.joinpath(dir, name)) + mtime[name] = stat and stat.mtime.sec or 0 + end + table.sort(names, function(a, b) + return mtime[a] > mtime[b] + end) + vim.api.nvim_buf_set_lines(args.buf, 0, -1, true, names) + end, + }) +< + Replacing the directory browser *dir-disable* To use another directory browser for the current session, delete the diff --git a/runtime/lua/nvim/dir.lua b/runtime/lua/nvim/dir.lua index 79145525d2..c3de19396d 100644 --- a/runtime/lua/nvim/dir.lua +++ b/runtime/lua/nvim/dir.lua @@ -187,6 +187,27 @@ local function set_maps(buf) map('R', '(nvim-dir-reload)') end +--- Let handlers reshape the rendered listing. Unlocks the buffer for the duration and +--- restores it afterwards, so a handler can sort, filter, or delete lines with ordinary +--- commands. +---@param buf integer +---@return boolean +local function exec_render_autocmd(buf) + if not set_buf_options(buf, { { 'readonly', false }, { 'modifiable', true } }) then + return false + end + -- Errors in handlers are reported by the autocmd machinery; swallow them here so the + -- buffer is never left writable. + pcall(api.nvim_buf_call, buf, function() + api.nvim_exec_autocmds('User', { pattern = 'DirReadPost', modeline = false }) + end) + return set_buf_options(buf, { + { 'modified', false }, + { 'readonly', true }, + { 'modifiable', false }, + }) +end + ---@param buf integer local function setup_render_autocmds(buf) api.nvim_clear_autocmds({ group = listing_group, buffer = buf }) @@ -246,14 +267,19 @@ function load(buf, name, provider, restore_view, setup, select) end return end + current_state.err, current_state.provider = err, provider + vim.b[buf].nvim_dir = current_state + + -- Runs before the cursor is placed, so handlers may reorder entries. + if not exec_render_autocmd(buf) then + return + end if restore_view and api.nvim_get_current_buf() == buf then vim.fn.winrestview(restore_view) end if select and api.nvim_get_current_buf() == buf then select_entry(select) end - current_state.err, current_state.provider = err, provider - vim.b[buf].nvim_dir = current_state if not setup then return diff --git a/test/functional/plugin/dir_spec.lua b/test/functional/plugin/dir_spec.lua index e7028bb914..27adcf4116 100644 --- a/test/functional/plugin/dir_spec.lua +++ b/test/functional/plugin/dir_spec.lua @@ -332,6 +332,83 @@ describe('nvim.dir', function() eq({ 'file2.txt' }, lines()) end) + it('fires DirReadPost for every listing', function() + make_fixture() + n.clear({ args = { '--clean' } }) + exec_lua(function() + vim.g.renders = 0 + vim.api.nvim_create_autocmd('User', { + pattern = 'DirReadPost', + callback = function(args) + vim.g.renders = vim.g.renders + 1 + vim.g.render_lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, true) + vim.g.render_writable = vim.bo[args.buf].modifiable and not vim.bo[args.buf].readonly + end, + }) + end) + + edit(root) + eq(1, exec_lua('return vim.g.renders')) + eq({ 'subdir/', '.hidden', 'alpha.txt' }, exec_lua('return vim.g.render_lines')) + -- Writable while handlers run, locked again afterwards. + eq(true, exec_lua('return vim.g.render_writable')) + eq(false, bufopt('modifiable')) + eq(true, bufopt('readonly')) + eq(false, bufopt('modified')) + + feed('R') + poke_eventloop() + eq(2, exec_lua('return vim.g.renders')) + + edit(root) + eq(3, exec_lua('return vim.g.renders')) + + api.nvim_win_set_cursor(0, { line_of('subdir/'), 0 }) + feed('') + poke_eventloop() + assert_directory(subdir) + eq(4, exec_lua('return vim.g.renders')) + end) + + it('keeps the listing readonly when a handler errors', function() + make_fixture() + n.clear({ args = { '--clean' } }) + exec_lua(function() + vim.api.nvim_create_autocmd('User', { + pattern = 'DirReadPost', + callback = function() + error('handler error') + end, + }) + end) + + edit(root) + + eq({ 'subdir/', '.hidden', 'alpha.txt' }, lines()) + eq(false, bufopt('modifiable')) + eq(true, bufopt('readonly')) + end) + + it('selects the origin entry after a handler reorders', function() + make_fixture() + n.clear({ args = { '--clean' } }) + exec_lua(function() + vim.api.nvim_create_autocmd('User', { + pattern = 'DirReadPost', + callback = function() + vim.cmd('silent keeppatterns sort!') + end, + }) + end) + + edit(subdir) + feed('-') + poke_eventloop() + + eq({ 'subdir/', 'alpha.txt', '.hidden' }, lines()) + eq('subdir/', api.nvim_get_current_line()) + end) + it('ignores callbacks from replaced listings', function() n.clear({ args = { '--clean' } })