From fa3820ebf1e8ccf5c7c0f3626d499b2c1aa8bc50 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 19 Apr 2024 14:16:23 -0400 Subject: [PATCH 001/206] feat: can restore Oil progress window when minimized --- lua/oil/init.lua | 8 +++++ lua/oil/mutator/init.lua | 11 +++++- lua/oil/mutator/progress.lua | 66 +++++++++++++++++++++++++----------- tests/manual_progress.lua | 28 +++++++++++++++ 4 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 tests/manual_progress.lua diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 30215b6..207ff50 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -955,6 +955,14 @@ M.setup = function(opts) elseif v == "--trash" then trash = true table.remove(args.fargs, i) + elseif v == "--progress" then + local mutator = require("oil.mutator") + if mutator.is_mutating() then + mutator.show_progress() + else + vim.notify("No mutation in progress", vim.log.levels.WARN) + end + return else i = i + 1 end diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index d76413a..83e71cc 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -364,6 +364,8 @@ M.enforce_action_order = function(actions) return ret end +local progress + ---@param actions oil.Action[] ---@param cb fun(err: nil|string) M.process_actions = function(actions, cb) @@ -390,11 +392,12 @@ M.process_actions = function(actions, cb) end local finished = false - local progress = Progress.new() + progress = Progress.new() local function finish(err) if not finished then finished = true progress:close() + progress = nil vim.api.nvim_exec_autocmds( "User", { pattern = "OilActionsPost", modeline = false, data = { err = err, actions = actions } } @@ -455,6 +458,12 @@ M.process_actions = function(actions, cb) next_action() end +M.show_progress = function() + if progress then + progress:restore() + end +end + local mutation_in_progress = false ---@return boolean diff --git a/lua/oil/mutator/progress.lua b/lua/oil/mutator/progress.lua index 1372c4e..58e8587 100644 --- a/lua/oil/mutator/progress.lua +++ b/lua/oil/mutator/progress.lua @@ -8,13 +8,11 @@ local Progress = {} local FPS = 20 function Progress.new() - local bufnr = vim.api.nvim_create_buf(false, true) - vim.bo[bufnr].bufhidden = "wipe" return setmetatable({ lines = { "", "" }, count = "", spinner = "", - bufnr = bufnr, + bufnr = nil, winid = nil, min_bufnr = nil, min_winid = nil, @@ -25,6 +23,15 @@ function Progress.new() }) end +---@private +---@return boolean +function Progress:is_minimized() + return not self.closing + and not self.bufnr + and self.min_bufnr + and vim.api.nvim_buf_is_valid(self.min_bufnr) +end + ---@param opts nil|table --- cancel fun() function Progress:show(opts) @@ -32,20 +39,24 @@ function Progress:show(opts) if self.winid and vim.api.nvim_win_is_valid(self.winid) then return end - self.closing = false - self.cancel = opts.cancel + local bufnr = vim.api.nvim_create_buf(false, true) + vim.bo[bufnr].bufhidden = "wipe" + self.bufnr = bufnr + self.cancel = opts.cancel or self.cancel local loading_iter = loading.get_bar_iter() local spinner = loading.get_iter("dots") - self.timer = vim.loop.new_timer() - self.timer:start( - 0, - math.floor(1000 / FPS), - vim.schedule_wrap(function() - self.lines[2] = string.format("%s %s", self.count, loading_iter()) - self.spinner = spinner() - self:_render() - end) - ) + if not self.timer then + self.timer = vim.loop.new_timer() + self.timer:start( + 0, + math.floor(1000 / FPS), + vim.schedule_wrap(function() + self.lines[2] = string.format("%s %s", self.count, loading_iter()) + self.spinner = spinner() + self:_render() + end) + ) + end local width, height = layout.calculate_dims(120, 10, config.progress) self.winid = vim.api.nvim_open_win(self.bufnr, true, { relative = "editor", @@ -89,6 +100,16 @@ function Progress:show(opts) vim.keymap.set("n", "M", minimize, { buffer = self.bufnr, nowait = true }) end +function Progress:restore() + if self.closing then + return + elseif not self:is_minimized() then + error("Cannot restore progress window: not minimized") + end + self:_cleanup_minimized_win() + self:show() +end + function Progress:_render() if self.bufnr and vim.api.nvim_buf_is_valid(self.bufnr) then util.render_text( @@ -139,6 +160,14 @@ function Progress:_cleanup_main_win() self.bufnr = nil end +function Progress:_cleanup_minimized_win() + if self.min_winid and vim.api.nvim_win_is_valid(self.min_winid) then + vim.api.nvim_win_close(self.min_winid, true) + end + self.min_winid = nil + self.min_bufnr = nil +end + function Progress:minimize() if self.closing then return @@ -160,6 +189,7 @@ function Progress:minimize() self.min_bufnr = bufnr self.min_winid = winid self:_render() + vim.notify_once("Restore progress window with :Oil --progress") end ---@param action oil.Action @@ -187,11 +217,7 @@ function Progress:close() self.timer = nil end self:_cleanup_main_win() - if self.min_winid and vim.api.nvim_win_is_valid(self.min_winid) then - vim.api.nvim_win_close(self.min_winid, true) - end - self.min_winid = nil - self.min_bufnr = nil + self:_cleanup_minimized_win() end return Progress diff --git a/tests/manual_progress.lua b/tests/manual_progress.lua new file mode 100644 index 0000000..bb838e2 --- /dev/null +++ b/tests/manual_progress.lua @@ -0,0 +1,28 @@ +-- Manual test for minimizing/restoring progress window +local Progress = require("oil.mutator.progress") + +local progress = Progress.new() + +progress:show({ + cancel = function() + progress:close() + end, +}) + +for i = 1, 10, 1 do + vim.defer_fn(function() + progress:set_action({ + type = "create", + url = string.format("oil:///tmp/test_%d.txt", i), + entry_type = "file", + }, i, 10) + end, (i - 1) * 1000) +end + +vim.defer_fn(function() + progress:close() +end, 10000) + +vim.keymap.set("n", "R", function() + progress:restore() +end, {}) From f41d7e7cd8e4028b03c35d847b4396790ac8bb2d Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 19 Apr 2024 15:26:34 -0400 Subject: [PATCH 002/206] fix: support visual mode when preview window is open (#315) --- README.md | 1 + doc/api.md | 14 ++- doc/oil.txt | 11 +- lua/oil/actions.lua | 2 +- lua/oil/init.lua | 235 +++++++++++++++++++++++++----------------- lua/oil/util.lua | 49 ++++++++- lua/oil/view.lua | 8 +- tests/altbuf_spec.lua | 2 +- 8 files changed, 219 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index 972409f..bbf4437 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i - [toggle_float(dir)](doc/api.md#toggle_floatdir) - [open(dir)](doc/api.md#opendir) - [close()](doc/api.md#close) +- [open_preview(opts)](doc/api.md#open_previewopts) - [select(opts, callback)](doc/api.md#selectopts-callback) - [save(opts, cb)](doc/api.md#saveopts-cb) - [setup(opts)](doc/api.md#setupopts) diff --git a/doc/api.md b/doc/api.md index 38d042d..7221036 100644 --- a/doc/api.md +++ b/doc/api.md @@ -14,6 +14,7 @@ - [toggle_float(dir)](#toggle_floatdir) - [open(dir)](#opendir) - [close()](#close) +- [open_preview(opts)](#open_previewopts) - [select(opts, callback)](#selectopts-callback) - [save(opts, cb)](#saveopts-cb) - [setup(opts)](#setupopts) @@ -116,6 +117,18 @@ Open oil browser for a directory Restore the buffer that was present when oil was opened +## open_preview(opts) + +`open_preview(opts)` \ +Preview the entry under the cursor in a split + +| Param | Type | Desc | | +| ----- | ------------ | -------------------------------------------------- | ------------------------------------- | +| opts | `nil\|table` | | | +| | vertical | `boolean` | Open the buffer in a vertical split | +| | horizontal | `boolean` | Open the buffer in a horizontal split | +| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | + ## select(opts, callback) `select(opts, callback)` \ @@ -127,7 +140,6 @@ Select the entry under the cursor | | vertical | `boolean` | Open the buffer in a vertical split | | | horizontal | `boolean` | Open the buffer in a horizontal split | | | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| | preview | `boolean` | Open the buffer in a preview window | | | tab | `boolean` | Open the buffer in a new tab | | | close | `boolean` | Close the original oil buffer once selection is made | | callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | | diff --git a/doc/oil.txt b/doc/oil.txt index 176a0cb..68a7531 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -282,6 +282,16 @@ close() *oil.clos Restore the buffer that was present when oil was opened +open_preview({opts}) *oil.open_preview* + Preview the entry under the cursor in a split + + Parameters: + {opts} `nil|table` + {vertical} `boolean` Open the buffer in a vertical split + {horizontal} `boolean` Open the buffer in a horizontal split + {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + modifier + select({opts}, {callback}) *oil.select* Select the entry under the cursor @@ -291,7 +301,6 @@ select({opts}, {callback}) *oil.selec {horizontal} `boolean` Open the buffer in a horizontal split {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split modifier - {preview} `boolean` Open the buffer in a preview window {tab} `boolean` Open the buffer in a new tab {close} `boolean` Close the original oil buffer once selection is made diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 1e3cdb3..cebd6b1 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -56,7 +56,7 @@ M.preview = { return end end - oil.select({ preview = true }) + oil.open_preview() end, } diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 207ff50..4fd4b68 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -5,6 +5,7 @@ local M = {} ---@field type oil.EntryType ---@field id nil|integer Will be nil if it hasn't been persisted to disk yet ---@field parsed_name nil|string +---@field meta nil|table ---@alias oil.EntryType "file"|"directory"|"socket"|"link"|"fifo" ---@alias oil.HlRange { [1]: string, [2]: integer, [3]: integer } A tuple of highlight group name, col_start, col_end @@ -372,13 +373,13 @@ local function update_preview_window(oil_bufnr) local util = require("oil.util") util.run_after_load(oil_bufnr, function() local cursor_entry = M.get_cursor_entry() - if cursor_entry then - local preview_win_id = util.get_preview_win() - if preview_win_id then - if cursor_entry.id ~= vim.w[preview_win_id].oil_entry_id then - M.select({ preview = true }) - end - end + local preview_win_id = util.get_preview_win() + if + cursor_entry + and preview_win_id + and cursor_entry.id ~= vim.w[preview_win_id].oil_entry_id + then + M.open_preview() end end) end @@ -437,22 +438,15 @@ M.close = function() vim.api.nvim_buf_delete(oilbuf, { force = true }) end ----Select the entry under the cursor +---Preview the entry under the cursor in a split ---@param opts nil|table --- vertical boolean Open the buffer in a vertical split --- horizontal boolean Open the buffer in a horizontal split --- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier ---- preview boolean Open the buffer in a preview window ---- tab boolean Open the buffer in a new tab ---- close boolean Close the original oil buffer once selection is made ----@param callback nil|fun(err: nil|string) Called once all entries have been opened -M.select = function(opts, callback) - local cache = require("oil.cache") - local config = require("oil.config") - local constants = require("oil.constants") - local pathutil = require("oil.pathutil") - local FIELD_META = constants.FIELD_META - opts = vim.tbl_extend("keep", opts or {}, {}) +M.open_preview = function(opts, callback) + opts = opts or {} + local util = require("oil.util") + local function finish(err) if err then vim.notify(err, vim.log.levels.ERROR) @@ -461,29 +455,135 @@ M.select = function(opts, callback) callback(err) end end - if opts.preview and not opts.horizontal and opts.vertical == nil then + + if not opts.horizontal and opts.vertical == nil then opts.vertical = true end - if not opts.split and (opts.horizontal or opts.vertical or opts.preview) then + if not opts.split then if opts.horizontal then opts.split = vim.o.splitbelow and "belowright" or "aboveleft" else opts.split = vim.o.splitright and "belowright" or "aboveleft" end end - if opts.tab and (opts.preview or opts.split) then - return finish("Cannot set preview or split when tab = true") - end - if opts.close and opts.preview then - return finish("Cannot use close=true with preview=true") - end - local util = require("oil.util") - if util.is_floating_win() and opts.preview then + if util.is_floating_win() then return finish("oil preview doesn't work in a floating window") end + + local entry = M.get_cursor_entry() + if not entry then + return finish("Could not find entry under cursor") + end + + local preview_win = util.get_preview_win() + local prev_win = vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_get_current_buf() + + local cmd = preview_win and "buffer" or "sbuffer" + local mods = { + vertical = opts.vertical, + horizontal = opts.horizontal, + split = opts.split, + } + + local is_visual_mode = util.is_visual_mode() + -- HACK Switching windows takes us out of visual mode. + -- Switching with nvim_set_current_win causes the previous visual selection (as used by `gv`) to + -- not get set properly. So we have to switch windows this way instead. + local hack_set_win = function(winid) + local winnr = vim.api.nvim_win_get_number(winid) + vim.cmd.wincmd({ args = { "w" }, count = winnr }) + end + + if preview_win then + if is_visual_mode then + hack_set_win(preview_win) + else + vim.api.nvim_set_current_win(preview_win) + end + end + + util.get_edit_path(bufnr, entry, function(normalized_url) + local filebufnr = vim.fn.bufadd(normalized_url) + local entry_is_file = not vim.endswith(normalized_url, "/") + + -- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after + -- we close the window + if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then + vim.bo[filebufnr].bufhidden = "wipe" + vim.b[filebufnr].oil_preview_buffer = true + end + + ---@diagnostic disable-next-line: param-type-mismatch + local ok, err = pcall(vim.cmd, { + cmd = cmd, + args = { filebufnr }, + mods = mods, + }) + -- Ignore swapfile errors + if not ok and err and not err:match("^Vim:E325:") then + vim.api.nvim_echo({ { err, "Error" } }, true, {}) + end + + vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 }) + vim.w.oil_entry_id = entry.id + vim.w.oil_source_win = prev_win + if is_visual_mode then + hack_set_win(prev_win) + -- Restore the visual selection + vim.cmd.normal({ args = { "gv" }, bang = true }) + else + vim.api.nvim_set_current_win(prev_win) + end + finish() + end) +end + +---Select the entry under the cursor +---@param opts nil|table +--- vertical boolean Open the buffer in a vertical split +--- horizontal boolean Open the buffer in a horizontal split +--- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier +--- tab boolean Open the buffer in a new tab +--- close boolean Close the original oil buffer once selection is made +---@param callback nil|fun(err: nil|string) Called once all entries have been opened +M.select = function(opts, callback) + local cache = require("oil.cache") + local config = require("oil.config") + local constants = require("oil.constants") + local util = require("oil.util") + local FIELD_META = constants.FIELD_META + opts = vim.tbl_extend("keep", opts or {}, {}) + + if opts.preview then + vim.notify_once( + "Deprecated: do not call oil.select with preview=true. Use oil.open_preview instead.\nThis shim will be removed on 2025-01-01" + ) + M.open_preview(opts, callback) + return + end + + local function finish(err) + if err then + vim.notify(err, vim.log.levels.ERROR) + end + if callback then + callback(err) + end + end + if not opts.split and (opts.horizontal or opts.vertical) then + if opts.horizontal then + opts.split = vim.o.splitbelow and "belowright" or "aboveleft" + else + opts.split = vim.o.splitright and "belowright" or "aboveleft" + end + end + if opts.tab and opts.split then + return finish("Cannot use split=true when tab = true") + end local adapter = util.get_adapter(0) if not adapter then - return finish("Could not find adapter for current buffer") + return finish("Not an oil buffer") end local visual_range = util.get_visual_range() @@ -506,10 +606,6 @@ M.select = function(opts, callback) if vim.tbl_isempty(entries) then return finish("Could not find entry under cursor") end - if #entries > 1 and opts.preview then - vim.notify("Cannot preview multiple entries", vim.log.levels.WARN) - entries = { entries[1] } - end -- Check if any of these entries are moved from their original location local bufname = vim.api.nvim_buf_get_name(0) @@ -533,7 +629,7 @@ M.select = function(opts, callback) end end end - if any_moved and not opts.preview and config.prompt_save_on_select_new_entry then + if any_moved and config.prompt_save_on_select_new_entry then local ok, choice = pcall(vim.fn.confirm, "Save changes?", "Yes\nNo", 1) if not ok then return finish() @@ -543,11 +639,8 @@ M.select = function(opts, callback) end end - local preview_win = util.get_preview_win() local prev_win = vim.api.nvim_get_current_win() - local scheme, dir = util.parse_url(bufname) - assert(scheme and dir) -- Async iter over entries so we can normalize the url before opening local i = 1 local function open_next_entry(cb) @@ -556,16 +649,7 @@ M.select = function(opts, callback) if not entry then return cb() end - local url = scheme .. dir .. entry.name - local is_directory = entry.type == "directory" - or ( - entry.type == "link" - and entry.meta - and entry.meta.link_stat - and entry.meta.link_stat.type == "directory" - ) - if is_directory then - url = url .. "/" + if util.is_directory(entry) then -- If this is a new directory BUT we think we already have an entry with this name, disallow -- entry. This prevents the case of MOVE /foo -> /bar + CREATE /foo. -- If you enter the new /foo, it will show the contents of the old /foo. @@ -573,29 +657,15 @@ M.select = function(opts, callback) return cb("Please save changes before entering new directory") end else + -- Close floating window before opening a file if vim.w.is_oil_win then vim.api.nvim_win_close(0, false) end end - local get_edit_path - if entry.name == ".." then - get_edit_path = function(edit_cb) - edit_cb(scheme .. pathutil.parent(dir)) - end - elseif adapter.get_entry_path then - get_edit_path = function(edit_cb) - adapter.get_entry_path(url, entry, edit_cb) - end - else - get_edit_path = function(edit_cb) - adapter.normalize_url(url, edit_cb) - end - end - -- Normalize the url before opening to prevent needing to rename them inside the BufReadCmd -- Renaming buffers during opening can lead to missed autocmds - get_edit_path(function(normalized_url) + util.get_edit_path(0, entry, function(normalized_url) local mods = { vertical = opts.vertical, horizontal = opts.horizontal, @@ -605,32 +675,17 @@ M.select = function(opts, callback) local filebufnr = vim.fn.bufadd(normalized_url) local entry_is_file = not vim.endswith(normalized_url, "/") - if opts.preview then - -- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after - -- we close the window - if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then - vim.bo[filebufnr].bufhidden = "wipe" - vim.b[filebufnr].oil_preview_buffer = true - end - elseif entry_is_file then - -- The :buffer command doesn't set buflisted=true - -- So do that for non-diretory-buffers + -- The :buffer command doesn't set buflisted=true + -- So do that for normal files or for oil dirs if config set buflisted=true + if entry_is_file or config.buf_options.buflisted then vim.bo[filebufnr].buflisted = true end - local cmd - if opts.preview and preview_win then - vim.api.nvim_set_current_win(preview_win) - cmd = "buffer" - else - if opts.tab then - vim.cmd.tabnew({ mods = mods }) - cmd = "buffer" - elseif opts.split then - cmd = "sbuffer" - else - cmd = "buffer" - end + local cmd = "buffer" + if opts.tab then + vim.cmd.tabnew({ mods = mods }) + elseif opts.split then + cmd = "sbuffer" end ---@diagnostic disable-next-line: param-type-mismatch local ok, err = pcall(vim.cmd, { @@ -643,12 +698,6 @@ M.select = function(opts, callback) vim.api.nvim_echo({ { err, "Error" } }, true, {}) end - if opts.preview then - vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 }) - vim.w.oil_entry_id = entry.id - vim.w.oil_source_win = prev_win - vim.api.nvim_set_current_win(prev_win) - end open_next_entry(cb) end) end diff --git a/lua/oil/util.lua b/lua/oil/util.lua index b19c003..211dd78 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -756,12 +756,16 @@ M.send_to_quickfix = function(opts) vim.api.nvim_exec_autocmds("QuickFixCmdPost", {}) end +---@return boolean +M.is_visual_mode = function() + local mode = vim.api.nvim_get_mode().mode + return mode:match("^[vV]") ~= nil +end + ---Get the current visual selection range. If not in visual mode, return nil. ---@return {start_lnum: integer, end_lnum: integer}? M.get_visual_range = function() - local mode = vim.api.nvim_get_mode().mode - local is_visual = mode:match("^[vV]") - if not is_visual then + if not M.is_visual_mode() then return end -- This is the best way to get the visual selection at the moment @@ -796,4 +800,43 @@ M.run_after_load = function(bufnr, callback) end end +---@param entry oil.Entry +---@return boolean +M.is_directory = function(entry) + local is_directory = entry.type == "directory" + or ( + entry.type == "link" + and entry.meta + and entry.meta.link_stat + and entry.meta.link_stat.type == "directory" + ) + return is_directory == true +end + +---Get the :edit path for an entry +---@param bufnr integer The oil buffer that contains the entry +---@param entry oil.Entry +---@param callback fun(normalized_url: string) +M.get_edit_path = function(bufnr, entry, callback) + local pathutil = require("oil.pathutil") + + local bufname = vim.api.nvim_buf_get_name(bufnr) + local scheme, dir = M.parse_url(bufname) + local adapter = M.get_adapter(bufnr) + assert(scheme and dir and adapter) + + local url = scheme .. dir .. entry.name + if M.is_directory(entry) then + url = url .. "/" + end + + if entry.name == ".." then + callback(scheme .. pathutil.parent(dir)) + elseif adapter.get_entry_path then + adapter.get_entry_path(url, entry, callback) + else + adapter.normalize_url(url, callback) + end +end + return M diff --git a/lua/oil/view.lua b/lua/oil/view.lua index a5032ee..adea825 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -389,7 +389,7 @@ M.initialize = function(bufnr) vim.schedule(constrain_cursor) end, }) - vim.api.nvim_create_autocmd("CursorMoved", { + vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, { desc = "Update oil preview window", group = "Oil", buffer = bufnr, @@ -420,11 +420,13 @@ M.initialize = function(bufnr) return end local entry = oil.get_cursor_entry() - if entry then + -- Don't update in visual mode. Visual mode implies editing not browsing, + -- and updating the preview can cause flicker and stutter. + if entry and not util.is_visual_mode() then local winid = util.get_preview_win() if winid then if entry.id ~= vim.w[winid].oil_entry_id then - oil.select({ preview = true }) + oil.open_preview() end end end diff --git a/tests/altbuf_spec.lua b/tests/altbuf_spec.lua index 2f1e8e3..4b51604 100644 --- a/tests/altbuf_spec.lua +++ b/tests/altbuf_spec.lua @@ -104,7 +104,7 @@ a.describe("Alternate buffer", function() return oil.get_cursor_entry() end, 10) vim.api.nvim_win_set_cursor(0, { 1, 1 }) - oil.select({ preview = true }) + oil.open_preview() test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) assert.equals("foo", vim.fn.expand("#")) end) From c86e48407b8a45f9aa8acb2b4512b384ea1eec84 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 19 Apr 2024 16:18:39 -0400 Subject: [PATCH 003/206] fix: race condition when entering oil buffer (#321) --- lua/oil/util.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/oil/util.lua b/lua/oil/util.lua index 211dd78..bd2f048 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -787,13 +787,12 @@ M.run_after_load = function(bufnr, callback) if vim.b[bufnr].oil_ready then callback() else - local autocmd_id - autocmd_id = vim.api.nvim_create_autocmd("User", { + vim.api.nvim_create_autocmd("User", { pattern = "OilEnter", callback = function(args) if args.data.buf == bufnr then - callback() - vim.api.nvim_del_autocmd(autocmd_id) + vim.api.nvim_buf_call(bufnr, callback) + return true end end, }) From 354c53080a6d7f4f0b2f0cc12e53bede2480b9e5 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 19 Apr 2024 17:05:19 -0400 Subject: [PATCH 004/206] fix: duplicate create actions (#334) --- lua/oil/mutator/init.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index 83e71cc..ccd23dd 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -61,10 +61,25 @@ M.create_actions_from_diffs = function(all_diffs) return list end, }) + + -- To deduplicate create actions + -- This can happen when creating deep nested files e.g. + -- > foo/bar/a.txt + -- > foo/bar/b.txt + local seen_creates = {} + ---@param action oil.Action local function add_action(action) local adapter = assert(config.get_adapter_by_scheme(action.dest_url or action.url)) if not adapter.filter_action or adapter.filter_action(action) then + if action.type == "create" then + if seen_creates[action.url] then + return + else + seen_creates[action.url] = true + end + end + table.insert(actions, action) end end From 1f05774e1c2dbc1940104b5c950d5c7b65ec6e0b Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 19 Apr 2024 17:54:21 -0400 Subject: [PATCH 005/206] feat: experimental support for git operations (#290) --- README.md | 13 +++++ doc/oil.txt | 13 +++++ lua/oil/adapters/files.lua | 31 +++++++++- lua/oil/config.lua | 13 +++++ lua/oil/git.lua | 115 +++++++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 lua/oil/git.lua diff --git a/README.md b/README.md index bbf4437..1d5fc4b 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,19 @@ require("oil").setup({ { "name", "asc" }, }, }, + -- EXPERIMENTAL support for performing file operations with git + git = { + -- Return true to automatically git add/mv/rm files + add = function(path) + return false + end, + mv = function(src_path, dest_path) + return false + end, + rm = function(path) + return false + end, + }, -- Configuration for the floating window in oil.open_float float = { -- Padding around the floating window diff --git a/doc/oil.txt b/doc/oil.txt index 68a7531..9e713ec 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -117,6 +117,19 @@ CONFIG *oil-confi { "name", "asc" }, }, }, + -- EXPERIMENTAL support for performing file operations with git + git = { + -- Return true to automatically git add/mv/rm files + add = function(path) + return false + end, + mv = function(src_path, dest_path) + return false + end, + rm = function(path) + return false + end, + }, -- Configuration for the floating window in oil.open_float float = { -- Padding around the floating window diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua index 7213153..89eb810 100644 --- a/lua/oil/adapters/files.lua +++ b/lua/oil/adapters/files.lua @@ -3,6 +3,7 @@ local columns = require("oil.columns") local config = require("oil.config") local constants = require("oil.constants") local fs = require("oil.fs") +local git = require("oil.git") local permissions = require("oil.adapters.files.permissions") local trash = require("oil.adapters.files.trash") local util = require("oil.util") @@ -476,6 +477,18 @@ M.perform_action = function(action, cb) local _, path = util.parse_url(action.url) assert(path) path = fs.posix_to_os_path(path) + + if config.git.add(path) then + local old_cb = cb + cb = vim.schedule_wrap(function(err) + if not err then + git.add(path, old_cb) + else + old_cb(err) + end + end) + end + if action.entry_type == "directory" then uv.fs_mkdir(path, 493, function(err) -- Ignore if the directory already exists @@ -503,6 +516,18 @@ M.perform_action = function(action, cb) local _, path = util.parse_url(action.url) assert(path) path = fs.posix_to_os_path(path) + + if config.git.rm(path) then + local old_cb = cb + cb = function(err) + if not err then + git.rm(path, old_cb) + else + old_cb(err) + end + end + end + if config.delete_to_trash then if config.trash_command then vim.notify_once( @@ -525,7 +550,11 @@ M.perform_action = function(action, cb) assert(dest_path) src_path = fs.posix_to_os_path(src_path) dest_path = fs.posix_to_os_path(dest_path) - fs.recursive_move(action.entry_type, src_path, dest_path, cb) + if config.git.mv(src_path, dest_path) then + git.mv(action.entry_type, src_path, dest_path, cb) + else + fs.recursive_move(action.entry_type, src_path, dest_path, cb) + end else -- We should never hit this because we don't implement supported_cross_adapter_actions cb("files adapter doesn't support cross-adapter move") diff --git a/lua/oil/config.lua b/lua/oil/config.lua index b368387..cc0ac3b 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -100,6 +100,19 @@ local default_config = { { "name", "asc" }, }, }, + -- EXPERIMENTAL support for performing file operations with git + git = { + -- Return true to automatically git add/mv/rm files + add = function(path) + return false + end, + mv = function(src_path, dest_path) + return false + end, + rm = function(path) + return false + end, + }, -- Configuration for the floating window in oil.open_float float = { -- Padding around the floating window diff --git a/lua/oil/git.lua b/lua/oil/git.lua new file mode 100644 index 0000000..b01c305 --- /dev/null +++ b/lua/oil/git.lua @@ -0,0 +1,115 @@ +-- integration with git operations +local fs = require("oil.fs") + +local M = {} + +---@param path string +---@return string|nil +M.get_root = function(path) + local git_dir = vim.fs.find(".git", { upward = true, path = path })[1] + if git_dir then + return vim.fs.dirname(git_dir) + else + return nil + end +end + +---@param path string +---@param cb fun(err: nil|string) +M.add = function(path, cb) + local root = M.get_root(path) + if not root then + return cb() + end + + local stderr = "" + local jid = vim.fn.jobstart({ "git", "add", path }, { + cwd = root, + stderr_buffered = true, + on_stderr = function(_, data) + stderr = table.concat(data, "\n") + end, + on_exit = function(_, code) + if code ~= 0 then + cb("Error in git add: " .. stderr) + else + cb() + end + end, + }) + if jid <= 0 then + cb() + end +end + +---@param path string +---@param cb fun(err: nil|string) +M.rm = function(path, cb) + local root = M.get_root(path) + if not root then + return cb() + end + + local stderr = "" + local jid = vim.fn.jobstart({ "git", "rm", "-r", path }, { + cwd = root, + stderr_buffered = true, + on_stderr = function(_, data) + stderr = table.concat(data, "\n") + end, + on_exit = function(_, code) + if code ~= 0 then + stderr = vim.trim(stderr) + if stderr:match("^fatal: pathspec '.*' did not match any files$") then + cb() + else + cb("Error in git rm: " .. stderr) + end + else + cb() + end + end, + }) + if jid <= 0 then + cb() + end +end + +---@param entry_type oil.EntryType +---@param src_path string +---@param dest_path string +---@param cb fun(err: nil|string) +M.mv = function(entry_type, src_path, dest_path, cb) + local src_git = M.get_root(src_path) + if not src_git or src_git ~= M.get_root(dest_path) then + fs.recursive_move(entry_type, src_path, dest_path, cb) + return + end + + local stderr = "" + local jid = vim.fn.jobstart({ "git", "mv", src_path, dest_path }, { + cwd = src_git, + stderr_buffered = true, + on_stderr = function(_, data) + stderr = table.concat(data, "\n") + end, + on_exit = function(_, code) + if code ~= 0 then + stderr = vim.trim(stderr) + if stderr:match("^fatal: not under version control") then + fs.recursive_move(entry_type, src_path, dest_path, cb) + else + cb("Error in git mv: " .. stderr) + end + else + cb() + end + end, + }) + if jid <= 0 then + -- Failed to run git, fall back to normal filesystem operations + fs.recursive_move(entry_type, src_path, dest_path, cb) + end +end + +return M From 2bc56ad68afd092af1b2e77dd5d61e156938564c Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 21 Apr 2024 07:37:54 -0700 Subject: [PATCH 006/206] fix: error when opening files from floating oil window (#355) --- lua/oil/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 4fd4b68..6050245 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -640,6 +640,7 @@ M.select = function(opts, callback) end local prev_win = vim.api.nvim_get_current_win() + local oil_bufnr = vim.api.nvim_get_current_buf() -- Async iter over entries so we can normalize the url before opening local i = 1 @@ -665,7 +666,7 @@ M.select = function(opts, callback) -- Normalize the url before opening to prevent needing to rename them inside the BufReadCmd -- Renaming buffers during opening can lead to missed autocmds - util.get_edit_path(0, entry, function(normalized_url) + util.get_edit_path(oil_bufnr, entry, function(normalized_url) local mods = { vertical = opts.vertical, horizontal = opts.horizontal, From a3c03e442a28c55ec2e9c61017541fc2d6433707 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 21 Apr 2024 07:48:03 -0700 Subject: [PATCH 007/206] test: add regression test for #355 --- tests/regression_spec.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/regression_spec.lua b/tests/regression_spec.lua index 20da321..8eff2b0 100644 --- a/tests/regression_spec.lua +++ b/tests/regression_spec.lua @@ -1,5 +1,6 @@ require("plenary.async").tests.add_to_env() local TmpDir = require("tests.tmpdir") +local actions = require("oil.actions") local oil = require("oil") local test_util = require("tests.test_util") local view = require("oil.view") @@ -131,4 +132,18 @@ a.describe("regression tests", function() ["baz.txt"] = "", }) end) + + -- https://github.com/stevearc/oil.nvim/issues/355 + a.it("can open files from floating window", function() + tmpdir:create({ "a.txt" }) + a.util.scheduler() + oil.open_float(tmpdir.path) + test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) + local oil_bufnr = vim.api.nvim_get_current_buf() + actions.select.callback() + vim.wait(1000, function() + return vim.fn.expand("%:t") == "a.txt" + end, 10) + assert.equals("a.txt", vim.fn.expand("%:t")) + end) end) From f41a0f24c009f71c6f112fce9c1f5d6c1f31847e Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 21 Apr 2024 07:55:19 -0700 Subject: [PATCH 008/206] lint: remove unused variable --- tests/regression_spec.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/regression_spec.lua b/tests/regression_spec.lua index 8eff2b0..9d8f944 100644 --- a/tests/regression_spec.lua +++ b/tests/regression_spec.lua @@ -139,7 +139,6 @@ a.describe("regression tests", function() a.util.scheduler() oil.open_float(tmpdir.path) test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) - local oil_bufnr = vim.api.nvim_get_current_buf() actions.select.callback() vim.wait(1000, function() return vim.fn.expand("%:t") == "a.txt" From 2edb43a7ecf11d5a237c1085128825f623f0bcfc Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 23 Apr 2024 19:21:02 -0700 Subject: [PATCH 009/206] ci: update checkout action --- .../automation_remove_question_label_on_comment.yml | 2 +- .github/workflows/tests.yml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/automation_remove_question_label_on_comment.yml b/.github/workflows/automation_remove_question_label_on_comment.yml index a188abf..f99bba8 100644 --- a/.github/workflows/automation_remove_question_label_on_comment.yml +++ b/.github/workflows/automation_remove_question_label_on_comment.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest if: github.event.sender.login != 'stevearc' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions-ecosystem/action-remove-labels@v1 with: labels: question diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 86dba71..4d9b69f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: name: Luacheck runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Prepare run: | @@ -29,7 +29,7 @@ jobs: name: StyLua runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Stylua uses: JohnnyMorganz/stylua-action@v3 with: @@ -41,7 +41,7 @@ jobs: name: typecheck runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: stevearc/nvim-typecheck-action@v1 with: path: lua @@ -58,7 +58,7 @@ jobs: env: NVIM_TAG: ${{ matrix.nvim_tag }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Neovim and dependencies run: | @@ -72,7 +72,7 @@ jobs: name: Update docs runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Neovim and dependencies run: | @@ -114,7 +114,7 @@ jobs: with: release-type: simple package-name: oil.nvim - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: rickstaa/action-create-tag@v1 if: ${{ steps.release.outputs.release_created }} with: From 6a7a10b6117aface6a25b54906140ad4f7fdabfc Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 23 Apr 2024 19:31:54 -0700 Subject: [PATCH 010/206] fix: git mv errors when moving empty directory (#358) --- lua/oil/git.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/oil/git.lua b/lua/oil/git.lua index b01c305..ec3b84b 100644 --- a/lua/oil/git.lua +++ b/lua/oil/git.lua @@ -96,7 +96,10 @@ M.mv = function(entry_type, src_path, dest_path, cb) on_exit = function(_, code) if code ~= 0 then stderr = vim.trim(stderr) - if stderr:match("^fatal: not under version control") then + if + stderr:match("^fatal: not under version control") + or stderr:match("^fatal: source directory is empty") + then fs.recursive_move(entry_type, src_path, dest_path, cb) else cb("Error in git mv: " .. stderr) From be0a1ecbf0541692a1b9b6e8ea15f5f57db8747a Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 23 Apr 2024 19:59:01 -0700 Subject: [PATCH 011/206] fix: gracefully handle new dirs with trailing backslash on windows (#336) --- lua/oil/mutator/parser.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/oil/mutator/parser.lua b/lua/oil/mutator/parser.lua index 43a95d0..cd94914 100644 --- a/lua/oil/mutator/parser.lua +++ b/lua/oil/mutator/parser.lua @@ -37,7 +37,7 @@ local FIELD_META = constants.FIELD_META ---@return string ---@return boolean local function parsedir(name) - local isdir = vim.endswith(name, "/") + local isdir = vim.endswith(name, "/") or (fs.is_windows and vim.endswith(name, "\\")) if isdir then name = name:sub(1, name:len() - 1) end From 96f0983e754694e592d4313f583cd31eaebfa80d Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 23 Apr 2024 20:20:58 -0700 Subject: [PATCH 012/206] fix(windows): file operation preview uses only backslash path separator (#336) --- lua/oil/adapters/files.lua | 2 +- lua/oil/util.lua | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua index 89eb810..cf6f73e 100644 --- a/lua/oil/adapters/files.lua +++ b/lua/oil/adapters/files.lua @@ -40,7 +40,7 @@ end M.to_short_os_path = function(path, entry_type) local shortpath = fs.shorten_path(fs.posix_to_os_path(path)) if entry_type == "directory" then - shortpath = util.addslash(shortpath) + shortpath = util.addslash(shortpath, true) end return shortpath end diff --git a/lua/oil/util.lua b/lua/oil/util.lua index bd2f048..a685b14 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -337,10 +337,16 @@ M.set_highlights = function(bufnr, highlights) end ---@param path string +---@param os_slash? boolean use os filesystem slash instead of posix slash ---@return string -M.addslash = function(path) - if not vim.endswith(path, "/") then - return path .. "/" +M.addslash = function(path, os_slash) + local slash = "/" + if os_slash and require("oil.fs").is_windows then + slash = "\\" + end + + if not vim.endswith(path, slash) then + return path .. slash else return path end From 3b3a6b23a120e69ddc980c9d32840ecd521fbff9 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 23 Apr 2024 20:21:56 -0700 Subject: [PATCH 013/206] fix(windows): treat both backslash and frontslash as path separators (#336) --- lua/oil/mutator/init.lua | 4 +++- lua/oil/mutator/parser.lua | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index ccd23dd..b4f5278 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -4,6 +4,7 @@ local cache = require("oil.cache") local columns = require("oil.columns") local config = require("oil.config") local constants = require("oil.constants") +local fs = require("oil.fs") local lsp_helpers = require("oil.lsp.helpers") local oil = require("oil") local parser = require("oil.mutator.parser") @@ -99,7 +100,8 @@ M.create_actions_from_diffs = function(all_diffs) table.insert(by_id, diff) else -- Parse nested files like foo/bar/baz - local pieces = vim.split(diff.name, "/") + local path_sep = fs.is_windows and "[/\\]" or "/" + local pieces = vim.split(diff.name, path_sep) local url = parent_url:gsub("/$", "") for i, v in ipairs(pieces) do local is_last = i == #pieces diff --git a/lua/oil/mutator/parser.lua b/lua/oil/mutator/parser.lua index cd94914..95e1c06 100644 --- a/lua/oil/mutator/parser.lua +++ b/lua/oil/mutator/parser.lua @@ -210,23 +210,26 @@ M.parse = function(bufnr) end local parsed_entry = result.data local entry = result.entry - if not parsed_entry.name or parsed_entry.name:match("/") or not entry then - local message - if not parsed_entry.name then - message = "No filename found" - elseif not entry then - message = "Could not find existing entry (was the ID changed?)" - else - message = "Filename cannot contain '/'" - end + + local err_message + if not parsed_entry.name then + err_message = "No filename found" + elseif not entry then + err_message = "Could not find existing entry (was the ID changed?)" + elseif parsed_entry.name:match("/") or parsed_entry.name:match(fs.sep) then + err_message = "Filename cannot contain path separator" + end + if err_message then table.insert(errors, { - message = message, + message = err_message, lnum = i - 1, end_lnum = i, col = 0, }) goto continue end + assert(entry) + check_dupe(parsed_entry.name, i) local meta = entry[FIELD_META] if original_entries[parsed_entry.name] == parsed_entry.id then From f3a31eba24587bc038592103d8f7e64648292115 Mon Sep 17 00:00:00 2001 From: Matthew Wilding Date: Wed, 24 Apr 2024 13:06:59 +0800 Subject: [PATCH 014/206] fix(windows): navigating into drive letter root directories (#341) * Fixed drive browsing on windows * Fixed naming * fix: Uppercase drive letter only * updated: Filter out network drives on windows * Update files.lua * Update files.lua * fixed: mapped drives * addslash to check for double slash * Fixed indents * Reverted addslash change * Fixed windows initial buffer name * Reverted formatting * Cleaned up callback * Fix addslash to handle \ too * Allow running tests workflow from fork * Fix workflow * Test * Tests * refactor: readability and comments * fix: convert buffer name to posix when hijacking directory --------- Co-authored-by: Steven Arcangeli --- lua/oil/adapters/files.lua | 25 +++++++++++++++++++++---- lua/oil/init.lua | 7 ++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua index cf6f73e..aab1750 100644 --- a/lua/oil/adapters/files.lua +++ b/lua/oil/adapters/files.lua @@ -214,19 +214,36 @@ end M.normalize_url = function(url, callback) local scheme, path = util.parse_url(url) assert(path) - if fs.is_windows and path == "/" then - return callback(url) + + if fs.is_windows then + if path == "/" then + return callback(url) + else + local is_root_drive = path:match("^/%u$") + if is_root_drive then + return callback(url .. "/") + end + end end + local os_path = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":p") uv.fs_realpath(os_path, function(err, new_os_path) - local realpath = new_os_path or os_path + local realpath + if fs.is_windows then + -- Ignore the fs_realpath on windows because it will resolve mapped network drives to the IP + -- address instead of using the drive letter + realpath = os_path + else + realpath = new_os_path or os_path + end + uv.fs_stat( realpath, vim.schedule_wrap(function(stat_err, stat) local is_directory if stat then is_directory = stat.type == "directory" - elseif vim.endswith(realpath, "/") then + elseif vim.endswith(realpath, "/") or (fs.is_windows and vim.endswith(realpath, "\\")) then is_directory = true else local filetype = vim.filetype.match({ filename = vim.fs.basename(realpath) }) diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 6050245..f334729 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -727,6 +727,7 @@ end ---@return boolean local function maybe_hijack_directory_buffer(bufnr) local config = require("oil.config") + local fs = require("oil.fs") local util = require("oil.util") if not config.default_file_explorer then return false @@ -738,10 +739,10 @@ local function maybe_hijack_directory_buffer(bufnr) if util.parse_url(bufname) or vim.fn.isdirectory(bufname) == 0 then return false end - local replaced = util.rename_buffer( - bufnr, - util.addslash(config.adapter_to_scheme.files .. vim.fn.fnamemodify(bufname, ":p")) + local new_name = util.addslash( + config.adapter_to_scheme.files .. fs.os_to_posix_path(vim.fn.fnamemodify(bufname, ":p")) ) + local replaced = util.rename_buffer(bufnr, new_name) return not replaced end From bcfc0a2e01def5019aa14fac2fc6de20dedb6d3d Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Wed, 1 May 2024 16:10:10 -0700 Subject: [PATCH 015/206] fix(ssh): garbled output when directory has broken symlinks The stderr was interleaving with the stdout when performing one of the ls operations. This was causing the parsing to sometimes fail and crash. --- lua/oil/adapters/ssh/sshfs.lua | 39 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/lua/oil/adapters/ssh/sshfs.lua b/lua/oil/adapters/ssh/sshfs.lua index c7cc4be..7cc3c65 100644 --- a/lua/oil/adapters/ssh/sshfs.lua +++ b/lua/oil/adapters/ssh/sshfs.lua @@ -175,28 +175,31 @@ function SSHFS:list_dir(url, path, callback) if any_links then -- If there were any soft links, then we need to run another ls command with -L so that we can -- resolve the type of the link target - self.conn:run("ls -aLl --color=never" .. path_postfix, function(link_err, link_lines) - -- Ignore exit code 1. That just means one of the links could not be resolved. - if link_err and not link_err:match("^1:") then - return callback(link_err) - end - assert(link_lines) - for _, line in ipairs(link_lines) do - if line ~= "" and not line:match("^total") then - local ok, name, type, meta = pcall(parse_ls_line, line) - if ok and name ~= "." and name ~= ".." then - local cache_entry = entries[name] - if cache_entry[FIELD_TYPE] == "link" then - cache_entry[FIELD_META].link_stat = { - type = type, - size = meta.size, - } + self.conn:run( + "ls -aLl --color=never" .. path_postfix .. " 2> /dev/null", + function(link_err, link_lines) + -- Ignore exit code 1. That just means one of the links could not be resolved. + if link_err and not link_err:match("^1:") then + return callback(link_err) + end + assert(link_lines) + for _, line in ipairs(link_lines) do + if line ~= "" and not line:match("^total") then + local ok, name, type, meta = pcall(parse_ls_line, line) + if ok and name ~= "." and name ~= ".." then + local cache_entry = entries[name] + if cache_entry[FIELD_TYPE] == "link" then + cache_entry[FIELD_META].link_stat = { + type = type, + size = meta.size, + } + end end end end + callback(nil, cache_entries) end - callback(nil, cache_entries) - end) + ) else callback(nil, cache_entries) end From 3abb6077d7d6b09f5eb794b8764223b3027f6807 Mon Sep 17 00:00:00 2001 From: ericguin <68578356+ericguin@users.noreply.github.com> Date: Mon, 6 May 2024 11:42:23 -0600 Subject: [PATCH 016/206] fix(ssh): config option to pass extra args to SCP (#340) * Adding in SCP options configuration This changeset adds in additional SCP options to the config. This allows the user to specify a list of flags to send to the SCP command that will be expanded into each shell command. The primary driver for this is from newe boxes SSHing into pre 9 openSSH boxes. New openSSH uses sftp server under the hood, rather than the older SCP protocol. If you go into a system that does not have these changes, SCP fails to work. The '-O' command line flag was introduced to resolve this. Using this change, the user can now pass in `extra_scp_options = {"-O"}` to resolve the issue. * Replacing table.unpack with global unpack * lint: apply stylua * refactor: change option name and shuffle config around --------- Co-authored-by: Eric Guinn Co-authored-by: Steven Arcangeli --- README.md | 10 ++++++---- doc/oil.txt | 10 ++++++---- lua/oil/adapters/ssh.lua | 24 +++++++++++++++++++----- lua/oil/config.lua | 10 ++++++---- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1d5fc4b..d96ed18 100644 --- a/README.md +++ b/README.md @@ -199,10 +199,6 @@ require("oil").setup({ ["g."] = "actions.toggle_hidden", ["g\\"] = "actions.toggle_trash", }, - -- Configuration for the floating keymaps help window - keymaps_help = { - border = "rounded", - }, -- Set to false to disable all of the above keymaps use_default_keymaps = true, view_options = { @@ -226,6 +222,8 @@ require("oil").setup({ { "name", "asc" }, }, }, + -- Extra arguments to pass to SCP when moving/copying files over SSH + extra_scp_args = {}, -- EXPERIMENTAL support for performing file operations with git git = { -- Return true to automatically git add/mv/rm files @@ -298,6 +296,10 @@ require("oil").setup({ ssh = { border = "rounded", }, + -- Configuration for the floating keymaps help window + keymaps_help = { + border = "rounded", + }, }) ``` diff --git a/doc/oil.txt b/doc/oil.txt index 9e713ec..2c6a5d0 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -90,10 +90,6 @@ CONFIG *oil-confi ["g."] = "actions.toggle_hidden", ["g\\"] = "actions.toggle_trash", }, - -- Configuration for the floating keymaps help window - keymaps_help = { - border = "rounded", - }, -- Set to false to disable all of the above keymaps use_default_keymaps = true, view_options = { @@ -117,6 +113,8 @@ CONFIG *oil-confi { "name", "asc" }, }, }, + -- Extra arguments to pass to SCP when moving/copying files over SSH + extra_scp_args = {}, -- EXPERIMENTAL support for performing file operations with git git = { -- Return true to automatically git add/mv/rm files @@ -189,6 +187,10 @@ CONFIG *oil-confi ssh = { border = "rounded", }, + -- Configuration for the floating keymaps help window + keymaps_help = { + border = "rounded", + }, }) < diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua index b307e25..06b5c3a 100644 --- a/lua/oil/adapters/ssh.lua +++ b/lua/oil/adapters/ssh.lua @@ -303,7 +303,14 @@ M.perform_action = function(action, cb) local src_conn = get_connection(action.src_url) local dest_conn = get_connection(action.dest_url) if src_conn ~= dest_conn then - shell.run({ "scp", "-C", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err) + shell.run({ + "scp", + unpack(config.extra_scp_args), + "-C", + "-r", + url_to_scp(src_res), + url_to_scp(dest_res), + }, function(err) if err then return cb(err) end @@ -322,7 +329,14 @@ M.perform_action = function(action, cb) local src_res = M.parse_url(action.src_url) local dest_res = M.parse_url(action.dest_url) if not url_hosts_equal(src_res, dest_res) then - shell.run({ "scp", "-C", "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb) + shell.run({ + "scp", + unpack(config.extra_scp_args), + "-C", + "-r", + url_to_scp(src_res), + url_to_scp(dest_res), + }, cb) else local src_conn = get_connection(action.src_url) src_conn:cp(src_res.path, dest_res.path, cb) @@ -341,7 +355,7 @@ M.perform_action = function(action, cb) src_arg = fs.posix_to_os_path(path) dest_arg = url_to_scp(M.parse_url(action.dest_url)) end - shell.run({ "scp", "-C", "-r", src_arg, dest_arg }, cb) + shell.run({ "scp", unpack(config.extra_scp_args), "-C", "-r", src_arg, dest_arg }, cb) end else cb(string.format("Bad action type: %s", action.type)) @@ -365,7 +379,7 @@ M.read_file = function(bufnr) end local tmp_bufnr = vim.fn.bufadd(tmpfile) - shell.run({ "scp", "-C", scp_url, tmpfile }, function(err) + shell.run({ "scp", unpack(config.extra_scp_args), "-C", scp_url, tmpfile }, function(err) loading.set_loading(bufnr, false) vim.bo[bufnr].modifiable = true vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } }) @@ -405,7 +419,7 @@ M.write_file = function(bufnr) vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } }) local tmp_bufnr = vim.fn.bufadd(tmpfile) - shell.run({ "scp", "-C", tmpfile, scp_url }, function(err) + shell.run({ "scp", unpack(config.extra_scp_args), "-C", tmpfile, scp_url }, function(err) vim.bo[bufnr].modifiable = true if err then vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR) diff --git a/lua/oil/config.lua b/lua/oil/config.lua index cc0ac3b..1defbe1 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -73,10 +73,6 @@ local default_config = { ["g."] = "actions.toggle_hidden", ["g\\"] = "actions.toggle_trash", }, - -- Configuration for the floating keymaps help window - keymaps_help = { - border = "rounded", - }, -- Set to false to disable all of the above keymaps use_default_keymaps = true, view_options = { @@ -100,6 +96,8 @@ local default_config = { { "name", "asc" }, }, }, + -- Extra arguments to pass to SCP when moving/copying files over SSH + extra_scp_args = {}, -- EXPERIMENTAL support for performing file operations with git git = { -- Return true to automatically git add/mv/rm files @@ -172,6 +170,10 @@ local default_config = { ssh = { border = "rounded", }, + -- Configuration for the floating keymaps help window + keymaps_help = { + border = "rounded", + }, } -- The adapter API hasn't really stabilized yet. We're not ready to advertise or encourage people to From 752563c59d64a5764cc0743d4fa0aac9ae4a2640 Mon Sep 17 00:00:00 2001 From: Oleg Kovalev Date: Tue, 7 May 2024 00:43:00 +0600 Subject: [PATCH 017/206] fix: icon column highlight parameter (#366) * fix: icon column highlight * fix: support icon highlight function --------- Co-authored-by: Steven Arcangeli --- lua/oil/columns.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua index 97d5e3c..3bdd286 100644 --- a/lua/oil/columns.lua +++ b/lua/oil/columns.lua @@ -205,19 +205,19 @@ end if has_devicons then M.register("icon", { render = function(entry, conf) - local type = entry[FIELD_TYPE] + local field_type = entry[FIELD_TYPE] local name = entry[FIELD_NAME] local meta = entry[FIELD_META] - if type == "link" and meta then + if field_type == "link" and meta then if meta.link then name = meta.link end if meta.link_stat then - type = meta.link_stat.type + field_type = meta.link_stat.type end end local icon, hl - if type == "directory" then + if field_type == "directory" then icon = conf and conf.directory or "" hl = "OilDirIcon" else @@ -230,6 +230,13 @@ if has_devicons then if not conf or conf.add_padding ~= false then icon = icon .. " " end + if conf and conf.highlight then + if type(conf.highlight) == "function" then + hl = conf.highlight(icon) + else + hl = conf.highlight + end + end return { icon, hl } end, From 010b44a79d497c697686e2727049d3dd215bbbd6 Mon Sep 17 00:00:00 2001 From: pn-watin <82295737+omelette-watin@users.noreply.github.com> Date: Mon, 6 May 2024 21:03:30 +0200 Subject: [PATCH 018/206] refactor: preview window uses Yes/No instead of Ok/Cancel (#344) NOTE: the `o` and `c` keymaps will continue to work. This only changes the text labels and adds new keymaps for `y` and `n`. * chore: replace ok and cancel with yes and no in confirmation window * chore: allow to configure labels and keymaps for confirmation window * chore: remove potential duplicate cancel keymaps * chore: update README and oil.txt * chore: nowait on confirm mappings and cleanup * refactor: fully transition to yes/no * move the config from under the `confirmation` key to the `preview` key, which is already in use for customizing the window * fully default to yes/no, keeping the o/c keybindings for backwards compatibility * make all of the `cancel` keybindings explicit (q, C-c, esc) * more dynamically choose highlighting of the action labels * refactor: just use yes/no and abandon configuration --------- Co-authored-by: Steven Arcangeli --- lua/oil/mutator/preview.lua | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/lua/oil/mutator/preview.lua b/lua/oil/mutator/preview.lua index 5e41898..64cd520 100644 --- a/lua/oil/mutator/preview.lua +++ b/lua/oil/mutator/preview.lua @@ -45,11 +45,12 @@ end ---@param bufnr integer ---@param lines string[] local function render_lines(winid, bufnr, lines) - util.render_text( - bufnr, - lines, - { v_align = "top", h_align = "left", winid = winid, actions = { "[O]k", "[C]ancel" } } - ) + util.render_text(bufnr, lines, { + v_align = "top", + h_align = "left", + winid = winid, + actions = { "[Y]es", "[N]o" }, + }) end ---@param actions oil.Action[] @@ -165,17 +166,22 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) end, }) ) - for _, cancel_key in ipairs({ "q", "C", "c", "", "" }) do + + -- We used to use [C]ancel to cancel, so preserve the old keymap + local cancel_keys = { "n", "N", "c", "C", "q", "", "" } + for _, cancel_key in ipairs(cancel_keys) do vim.keymap.set("n", cancel_key, function() cancel() end, { buffer = bufnr, nowait = true }) end - vim.keymap.set("n", "O", function() - confirm() - end, { buffer = bufnr }) - vim.keymap.set("n", "o", function() - confirm() - end, { buffer = bufnr }) + + -- We used to use [O]k to confirm, so preserve the old keymap + local confirm_keys = { "y", "Y", "o", "O" } + for _, confirm_key in ipairs(confirm_keys) do + vim.keymap.set("n", confirm_key, function() + confirm() + end, { buffer = bufnr, nowait = true }) + end end) return M From aa0c00c7fd51982ac476d165cd021f348cf5ea71 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Mon, 13 May 2024 11:35:07 -0600 Subject: [PATCH 019/206] fix(ssh): bad argument when editing files over ssh (#370) --- lua/oil/adapters/ssh.lua | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua index 06b5c3a..40275a7 100644 --- a/lua/oil/adapters/ssh.lua +++ b/lua/oil/adapters/ssh.lua @@ -20,6 +20,13 @@ local FIELD_META = constants.FIELD_META ---@field port nil|integer ---@field path string +---@param args string[] +local function scp(args, ...) + local cmd = vim.list_extend({ "scp", "-C" }, config.extra_scp_args) + vim.list_extend(cmd, args) + shell.run(cmd, ...) +end + ---@param oil_url string ---@return oil.sshUrl M.parse_url = function(oil_url) @@ -303,14 +310,7 @@ M.perform_action = function(action, cb) local src_conn = get_connection(action.src_url) local dest_conn = get_connection(action.dest_url) if src_conn ~= dest_conn then - shell.run({ - "scp", - unpack(config.extra_scp_args), - "-C", - "-r", - url_to_scp(src_res), - url_to_scp(dest_res), - }, function(err) + scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, function(err) if err then return cb(err) end @@ -329,14 +329,7 @@ M.perform_action = function(action, cb) local src_res = M.parse_url(action.src_url) local dest_res = M.parse_url(action.dest_url) if not url_hosts_equal(src_res, dest_res) then - shell.run({ - "scp", - unpack(config.extra_scp_args), - "-C", - "-r", - url_to_scp(src_res), - url_to_scp(dest_res), - }, cb) + scp({ "-r", url_to_scp(src_res), url_to_scp(dest_res) }, cb) else local src_conn = get_connection(action.src_url) src_conn:cp(src_res.path, dest_res.path, cb) @@ -355,7 +348,7 @@ M.perform_action = function(action, cb) src_arg = fs.posix_to_os_path(path) dest_arg = url_to_scp(M.parse_url(action.dest_url)) end - shell.run({ "scp", unpack(config.extra_scp_args), "-C", "-r", src_arg, dest_arg }, cb) + scp({ "-r", src_arg, dest_arg }, cb) end else cb(string.format("Bad action type: %s", action.type)) @@ -379,7 +372,7 @@ M.read_file = function(bufnr) end local tmp_bufnr = vim.fn.bufadd(tmpfile) - shell.run({ "scp", unpack(config.extra_scp_args), "-C", scp_url, tmpfile }, function(err) + scp({ scp_url, tmpfile }, function(err) loading.set_loading(bufnr, false) vim.bo[bufnr].modifiable = true vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } }) @@ -419,7 +412,7 @@ M.write_file = function(bufnr) vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } }) local tmp_bufnr = vim.fn.bufadd(tmpfile) - shell.run({ "scp", unpack(config.extra_scp_args), "-C", tmpfile, scp_url }, function(err) + scp({ tmpfile, scp_url }, function(err) vim.bo[bufnr].modifiable = true if err then vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR) From 3283deec96314326faed1d3a94ce142ae9773d8b Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Mon, 13 May 2024 20:02:11 -0600 Subject: [PATCH 020/206] lint: ignore some type errors --- lua/oil/adapters/files.lua | 2 +- lua/oil/adapters/trash/freedesktop.lua | 2 +- lua/oil/adapters/trash/mac.lua | 2 +- lua/oil/fs.lua | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua index aab1750..d502c90 100644 --- a/lua/oil/adapters/files.lua +++ b/lua/oil/adapters/files.lua @@ -343,7 +343,7 @@ M.list = function(url, column_defs, cb) local dir = fs.posix_to_os_path(path) local fetch_meta = columns.get_metadata_fetcher(M, column_defs) - ---@diagnostic disable-next-line: param-type-mismatch + ---@diagnostic disable-next-line: param-type-mismatch, discard-returns uv.fs_opendir(dir, function(open_err, fd) if open_err then if open_err:match("^ENOENT: no such file or directory") then diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua index 3109a04..24a6e68 100644 --- a/lua/oil/adapters/trash/freedesktop.lua +++ b/lua/oil/adapters/trash/freedesktop.lua @@ -242,7 +242,7 @@ M.list = function(url, column_defs, cb) end local info_dir = fs.join(trash_dir, "info") - ---@diagnostic disable-next-line: param-type-mismatch + ---@diagnostic disable-next-line: param-type-mismatch, discard-returns uv.fs_opendir(info_dir, function(open_err, fd) if open_err then if open_err:match("^ENOENT: no such file or directory") then diff --git a/lua/oil/adapters/trash/mac.lua b/lua/oil/adapters/trash/mac.lua index cda4a1e..8b2d33a 100644 --- a/lua/oil/adapters/trash/mac.lua +++ b/lua/oil/adapters/trash/mac.lua @@ -48,7 +48,7 @@ M.list = function(url, column_defs, cb) local _, path = util.parse_url(url) assert(path) local trash_dir = get_trash_dir() - ---@diagnostic disable-next-line: param-type-mismatch + ---@diagnostic disable-next-line: param-type-mismatch, discard-returns uv.fs_opendir(trash_dir, function(open_err, fd) if open_err then if open_err:match("^ENOENT: no such file or directory") then diff --git a/lua/oil/fs.lua b/lua/oil/fs.lua index 82def6c..ac216a1 100644 --- a/lua/oil/fs.lua +++ b/lua/oil/fs.lua @@ -163,7 +163,7 @@ end ---@param dir string ---@param cb fun(err: nil|string, entries: nil|{type: oil.EntryType, name: string}) M.listdir = function(dir, cb) - ---@diagnostic disable-next-line: param-type-mismatch + ---@diagnostic disable-next-line: param-type-mismatch, discard-returns uv.fs_opendir(dir, function(open_err, fd) if open_err then return cb(open_err) @@ -203,7 +203,7 @@ M.recursive_delete = function(entry_type, path, cb) if entry_type ~= "directory" then return uv.fs_unlink(path, cb) end - ---@diagnostic disable-next-line: param-type-mismatch + ---@diagnostic disable-next-line: param-type-mismatch, discard-returns uv.fs_opendir(path, function(open_err, fd) if open_err then return cb(open_err) @@ -273,7 +273,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb) if mkdir_err then return cb(mkdir_err) end - ---@diagnostic disable-next-line: param-type-mismatch + ---@diagnostic disable-next-line: param-type-mismatch, discard-returns uv.fs_opendir(src_path, function(open_err, fd) if open_err then return cb(open_err) From f630887cd845a7341bc16488fe8aaecffe3aaa8a Mon Sep 17 00:00:00 2001 From: Steven Arcangeli <506791+stevearc@users.noreply.github.com> Date: Tue, 14 May 2024 22:55:18 -0600 Subject: [PATCH 021/206] fix(windows): convert posix paths before matching LSP watch globs (#374) --- lua/oil/lsp/helpers.lua | 10 ++++++++++ lua/oil/lsp/workspace.lua | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/lua/oil/lsp/helpers.lua b/lua/oil/lsp/helpers.lua index 89c5469..45264dc 100644 --- a/lua/oil/lsp/helpers.lua +++ b/lua/oil/lsp/helpers.lua @@ -1,4 +1,5 @@ local config = require("oil.config") +local fs = require("oil.fs") local util = require("oil.util") local workspace = require("oil.lsp.workspace") @@ -17,23 +18,32 @@ M.will_perform_file_operations = function(actions) local src_adapter = assert(config.get_adapter_by_scheme(src_scheme)) local dest_scheme, dest_path = util.parse_url(action.dest_url) local dest_adapter = assert(config.get_adapter_by_scheme(dest_scheme)) + src_path = fs.posix_to_os_path(src_path) + dest_path = fs.posix_to_os_path(assert(dest_path)) if src_adapter.name == "files" and dest_adapter.name == "files" then moves[src_path] = dest_path + elseif src_adapter.name == "files" then + table.insert(deletes, src_path) + elseif dest_adapter.name == "files" then + table.insert(creates, src_path) end elseif action.type == "create" then local scheme, path = util.parse_url(action.url) + path = fs.posix_to_os_path(assert(path)) local adapter = assert(config.get_adapter_by_scheme(scheme)) if adapter.name == "files" then table.insert(creates, path) end elseif action.type == "delete" then local scheme, path = util.parse_url(action.url) + path = fs.posix_to_os_path(assert(path)) local adapter = assert(config.get_adapter_by_scheme(scheme)) if adapter.name == "files" then table.insert(deletes, path) end elseif action.type == "copy" then local scheme, path = util.parse_url(action.dest_url) + path = fs.posix_to_os_path(assert(path)) local adapter = assert(config.get_adapter_by_scheme(scheme)) if adapter.name == "files" then table.insert(creates, path) diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua index 49065a0..9879e72 100644 --- a/lua/oil/lsp/workspace.lua +++ b/lua/oil/lsp/workspace.lua @@ -65,6 +65,12 @@ local function get_matching_paths(client, filters, paths) if ignore_case then glob = glob:lower() end + + -- Some language servers use forward slashes as path separators on Windows (LuaLS) + if fs.is_windows then + glob = glob:gsub("/", "\\") + end + ---@type nil|vim.lpeg.Pattern local glob_pattern = vim.glob and vim.glob.to_lpeg and vim.glob.to_lpeg(glob) local matches = pattern.matches From 8a2de6ada22641855a843316a761fdf03744bb7c Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Thu, 16 May 2024 10:46:31 -0700 Subject: [PATCH 022/206] ci: update stylua version to v0.20.0 --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d9b69f..20f2adf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,10 +31,10 @@ jobs: steps: - uses: actions/checkout@v4 - name: Stylua - uses: JohnnyMorganz/stylua-action@v3 + uses: JohnnyMorganz/stylua-action@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.18.2 + version: v0.20.0 args: --check lua tests typecheck: From 27d9f371612822fb0b7b25099b65b1f6b9c60dfb Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Thu, 16 May 2024 10:47:29 -0700 Subject: [PATCH 023/206] ci: run tests against Neovim v0.10.0 --- .github/workflows/tests.yml | 1 + lua/oil/adapters/ssh.lua | 8 +- lua/oil/init.lua | 9 +- lua/oil/lsp/_types.lua | 224 ------------------------------------ lua/oil/lsp/workspace.lua | 5 +- 5 files changed, 16 insertions(+), 231 deletions(-) delete mode 100644 lua/oil/lsp/_types.lua diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20f2adf..ebd7266 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,6 +52,7 @@ jobs: include: - nvim_tag: v0.8.3 - nvim_tag: v0.9.4 + - nvim_tag: v0.10.0 name: Run tests runs-on: ubuntu-22.04 diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua index 40275a7..c52f5a9 100644 --- a/lua/oil/adapters/ssh.lua +++ b/lua/oil/adapters/ssh.lua @@ -364,7 +364,9 @@ M.read_file = function(bufnr) local url = M.parse_url(bufname) local scp_url = url_to_scp(url) local basename = pathutil.basename(bufname) - local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil") + local cache_dir = vim.fn.stdpath("cache") + assert(type(cache_dir) == "string") + local tmpdir = fs.join(cache_dir, "oil") fs.mkdirp(tmpdir) local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXX")) if fd then @@ -402,7 +404,9 @@ M.write_file = function(bufnr) local bufname = vim.api.nvim_buf_get_name(bufnr) local url = M.parse_url(bufname) local scp_url = url_to_scp(url) - local tmpdir = fs.join(vim.fn.stdpath("cache"), "oil") + local cache_dir = vim.fn.stdpath("cache") + assert(type(cache_dir) == "string") + local tmpdir = fs.join(cache_dir, "oil") local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "ssh_XXXXXXXX")) if fd then vim.loop.fs_close(fd) diff --git a/lua/oil/init.lua b/lua/oil/init.lua index f334729..737f21c 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -349,7 +349,7 @@ M.open_float = function(dir) vim.cmd.edit({ args = { util.escape_filename(parent_url) }, mods = { keepalt = true } }) -- :edit will set buflisted = true, but we may not want that if config.buf_options.buflisted ~= nil then - vim.api.nvim_buf_set_option(0, "buflisted", config.buf_options.buflisted) + vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 }) end if vim.fn.has("nvim-0.9") == 0 then @@ -400,7 +400,7 @@ M.open = function(dir) vim.cmd.edit({ args = { util.escape_filename(parent_url) }, mods = { keepalt = true } }) -- :edit will set buflisted = true, but we may not want that if config.buf_options.buflisted ~= nil then - vim.api.nvim_buf_set_option(0, "buflisted", config.buf_options.buflisted) + vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 }) end -- If preview window exists, update its content @@ -835,8 +835,11 @@ local function set_colors() end -- TODO can remove this call once we drop support for Neovim 0.8. FloatTitle was introduced as a -- built-in highlight group in 0.9, and we can start to rely on colorschemes setting it. - if not pcall(vim.api.nvim_get_hl_by_name, "FloatTitle", true) then + ---@diagnostic disable-next-line: deprecated + if vim.fn.has("nvim-0.9") == 0 and not pcall(vim.api.nvim_get_hl_by_name, "FloatTitle", true) then + ---@diagnostic disable-next-line: deprecated local border = vim.api.nvim_get_hl_by_name("FloatBorder", true) + ---@diagnostic disable-next-line: deprecated local normal = vim.api.nvim_get_hl_by_name("Normal", true) vim.api.nvim_set_hl( 0, diff --git a/lua/oil/lsp/_types.lua b/lua/oil/lsp/_types.lua deleted file mode 100644 index f4603fb..0000000 --- a/lua/oil/lsp/_types.lua +++ /dev/null @@ -1,224 +0,0 @@ --- LSP types copied from Neovim core to make typechecking pass - ----@alias lsp.null nil ----@alias uinteger integer ----@alias lsp.decimal number ----@alias lsp.DocumentUri string ----@alias lsp.URI string ----@alias lsp.LSPObject table ----@alias lsp.LSPArray lsp.LSPAny[] ----@alias lsp.LSPAny lsp.LSPObject|lsp.LSPArray|string|number|boolean|nil - ----An identifier to refer to a change annotation stored with a workspace edit. ----@alias lsp.ChangeAnnotationIdentifier string - ----A pattern kind describing if a glob pattern matches a file a folder or ----both. ---- ----@since 3.16.0 ----@alias lsp.FileOperationPatternKind ----| "file" # file ----| "folder" # folder - ----Matching options for the file operation pattern. ---- ----@since 3.16.0 ----@class lsp.FileOperationPatternOptions ----The pattern should be matched ignoring casing. ----@field ignoreCase? boolean - ----Describes textual changes on a text document. A TextDocumentEdit describes all changes ----on a document version Si and after they are applied move the document to version Si+1. ----So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any ----kind of ordering. However the edits must be non overlapping. ----@class lsp.TextDocumentEdit ----The text document to change. ----@field textDocument lsp.OptionalVersionedTextDocumentIdentifier ----The edits to be applied. ---- ----@since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a ----client capability. ----@field edits lsp.TextEdit|lsp.AnnotatedTextEdit[] - ----A literal to identify a text document in the client. ----@class lsp.TextDocumentIdentifier ----The text document's uri. ----@field uri lsp.DocumentUri - ----A text document identifier to optionally denote a specific version of a text document. ----@class lsp.OptionalVersionedTextDocumentIdentifier: lsp.TextDocumentIdentifier ----The version number of this document. If a versioned text document identifier ----is sent from the server to the client and the file is not open in the editor ----(the server has not received an open notification before) the server can send ----`null` to indicate that the version is unknown and the content on disk is the ----truth (as specified with document content ownership). ----@field version integer|lsp.null - ----A special text edit with an additional change annotation. ---- ----@since 3.16.0. ----@class lsp.AnnotatedTextEdit: lsp.TextEdit ----The actual identifier of the change annotation ----@field annotationId lsp.ChangeAnnotationIdentifier - ----A workspace edit represents changes to many resources managed in the workspace. The edit ----should either provide `changes` or `documentChanges`. If documentChanges are present ----they are preferred over `changes` if the client can handle versioned document edits. ---- ----Since version 3.13.0 a workspace edit can contain resource operations as well. If resource ----operations are present clients need to execute the operations in the order in which they ----are provided. So a workspace edit for example can consist of the following two changes: ----(1) a create file a.txt and (2) a text document edit which insert text into file a.txt. ---- ----An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will ----cause failure of the operation. How the client recovers from the failure is described by ----the client capability: `workspace.workspaceEdit.failureHandling` ----@class lsp.WorkspaceEdit ----Holds changes to existing resources. ----@field changes? table ----Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes ----are either an array of `TextDocumentEdit`s to express changes to n different text documents ----where each text document edit addresses a specific version of a text document. Or it can contain ----above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. ---- ----Whether a client supports versioned document edits is expressed via ----`workspace.workspaceEdit.documentChanges` client capability. ---- ----If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then ----only plain `TextEdit`s using the `changes` property are supported. ----@field documentChanges? (lsp.TextDocumentEdit|lsp.CreateFile|lsp.RenameFile|lsp.DeleteFile)[] ----A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and ----delete file / folder operations. ---- ----Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. ---- ----@since 3.16.0 ----@field changeAnnotations? table - ----Additional information that describes document changes. ---- ----@since 3.16.0 ----@class lsp.ChangeAnnotation ----A human-readable string describing the actual change. The string ----is rendered prominent in the user interface. ----@field label string ----A flag which indicates that user confirmation is needed ----before applying the change. ----@field needsConfirmation? boolean ----A human-readable string which is rendered less prominent in ----the user interface. ----@field description? string - ----A text edit applicable to a text document. ----@class lsp.TextEdit ----The range of the text document to be manipulated. To insert ----text into a document create a range where start === end. ----@field range lsp.Range ----The string to be inserted. For delete operations use an ----empty string. ----@field newText string - ----Options to create a file. ----@class lsp.CreateFileOptions ----Overwrite existing file. Overwrite wins over `ignoreIfExists` ----@field overwrite? boolean ----Ignore if exists. ----@field ignoreIfExists? boolean - ----Rename file options ----@class lsp.RenameFileOptions ----Overwrite target if existing. Overwrite wins over `ignoreIfExists` ----@field overwrite? boolean ----Ignores if target exists. ----@field ignoreIfExists? boolean - ----Delete file options ----@class lsp.DeleteFileOptions ----Delete the content recursively if a folder is denoted. ----@field recursive? boolean ----Ignore the operation if the file doesn't exist. ----@field ignoreIfNotExists? boolean - ----A generic resource operation. ----@class lsp.ResourceOperation ----The resource operation kind. ----@field kind string ----An optional annotation identifier describing the operation. ---- ----@since 3.16.0 ----@field annotationId? lsp.ChangeAnnotationIdentifier - ----Create file operation. ----@class lsp.CreateFile: lsp.ResourceOperation ----A create ----@field kind "create" ----The resource to create. ----@field uri lsp.DocumentUri ----Additional options ----@field options? lsp.CreateFileOptions - ----Rename file operation ----@class lsp.RenameFile: lsp.ResourceOperation ----A rename ----@field kind "rename" ----The old (existing) location. ----@field oldUri lsp.DocumentUri ----The new location. ----@field newUri lsp.DocumentUri ----Rename options. ----@field options? lsp.RenameFileOptions - ----Delete file operation ----@class lsp.DeleteFile: lsp.ResourceOperation ----A delete ----@field kind "delete" ----The file to delete. ----@field uri lsp.DocumentUri ----Delete options. ----@field options? lsp.DeleteFileOptions - ----A pattern to describe in which file operation requests or notifications ----the server is interested in receiving. ---- ----@since 3.16.0 ----@class lsp.FileOperationPattern ----The glob pattern to match. Glob patterns can have the following syntax: ----- `*` to match one or more characters in a path segment ----- `?` to match on one character in a path segment ----- `**` to match any number of path segments, including none ----- `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) ----- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) ----- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) ----@field glob string ----Whether to match files or folders with this pattern. ---- ----Matches both if undefined. ----@field matches? lsp.FileOperationPatternKind ----Additional options used during matching. ----@field options? lsp.FileOperationPatternOptions - ----A filter to describe in which file operation requests or notifications ----the server is interested in receiving. ---- ----@since 3.16.0 ----@class lsp.FileOperationFilter ----A Uri scheme like `file` or `untitled`. ----@field scheme? string ----The actual file operation pattern. ----@field pattern lsp.FileOperationPattern - ---- @class vim.lpeg.Pattern ---- @operator unm: vim.lpeg.Pattern ---- @operator add(vim.lpeg.Pattern): vim.lpeg.Pattern ---- @operator sub(vim.lpeg.Pattern): vim.lpeg.Pattern ---- @operator mul(vim.lpeg.Pattern): vim.lpeg.Pattern ---- @operator mul(vim.lpeg.Capture): vim.lpeg.Pattern ---- @operator div(string): vim.lpeg.Capture ---- @operator div(number): vim.lpeg.Capture ---- @operator div(table): vim.lpeg.Capture ---- @operator div(function): vim.lpeg.Capture ---- @operator pow(number): vim.lpeg.Pattern ---- @operator mod(function): nil ---- @field match fun(pattern: vim.lpeg.Pattern, subject: string, init?: integer): integer|vim.lpeg.Capture|nil - ---- @alias vim.lpeg.Capture vim.lpeg.Pattern diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua index 9879e72..79ab784 100644 --- a/lua/oil/lsp/workspace.lua +++ b/lua/oil/lsp/workspace.lua @@ -14,11 +14,12 @@ end local M = {} ---@param method string ----@return lsp.Client[] +---@return vim.lsp.Client[] local function get_clients(method) if vim.fn.has("nvim-0.10") == 1 then return vim.lsp.get_clients({ method = method }) else + ---@diagnostic disable-next-line: deprecated local clients = vim.lsp.get_active_clients() return vim.tbl_filter(function(client) return client.supports_method(method) @@ -47,7 +48,7 @@ local function match_glob(glob, path) return match >= 0 end ----@param client lsp.Client +---@param client vim.lsp.Client ---@param filters nil|lsp.FileOperationFilter[] ---@param paths string[] ---@return nil|string[] From 80eb2d6719649ea597b4c14a98f66d38960fd89c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 11:32:12 -0700 Subject: [PATCH 024/206] chore(master): release 2.9.0 (#354) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8f445..b9d4660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## [2.9.0](https://github.com/stevearc/oil.nvim/compare/v2.8.0...v2.9.0) (2024-05-16) + + +### Features + +* can restore Oil progress window when minimized ([fa3820e](https://github.com/stevearc/oil.nvim/commit/fa3820ebf1e8ccf5c7c0f3626d499b2c1aa8bc50)) +* experimental support for git operations ([#290](https://github.com/stevearc/oil.nvim/issues/290)) ([1f05774](https://github.com/stevearc/oil.nvim/commit/1f05774e1c2dbc1940104b5c950d5c7b65ec6e0b)) + + +### Bug Fixes + +* duplicate create actions ([#334](https://github.com/stevearc/oil.nvim/issues/334)) ([354c530](https://github.com/stevearc/oil.nvim/commit/354c53080a6d7f4f0b2f0cc12e53bede2480b9e5)) +* error when opening files from floating oil window ([#355](https://github.com/stevearc/oil.nvim/issues/355)) ([2bc56ad](https://github.com/stevearc/oil.nvim/commit/2bc56ad68afd092af1b2e77dd5d61e156938564c)) +* git mv errors when moving empty directory ([#358](https://github.com/stevearc/oil.nvim/issues/358)) ([6a7a10b](https://github.com/stevearc/oil.nvim/commit/6a7a10b6117aface6a25b54906140ad4f7fdabfc)) +* gracefully handle new dirs with trailing backslash on windows ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([be0a1ec](https://github.com/stevearc/oil.nvim/commit/be0a1ecbf0541692a1b9b6e8ea15f5f57db8747a)) +* icon column highlight parameter ([#366](https://github.com/stevearc/oil.nvim/issues/366)) ([752563c](https://github.com/stevearc/oil.nvim/commit/752563c59d64a5764cc0743d4fa0aac9ae4a2640)) +* race condition when entering oil buffer ([#321](https://github.com/stevearc/oil.nvim/issues/321)) ([c86e484](https://github.com/stevearc/oil.nvim/commit/c86e48407b8a45f9aa8acb2b4512b384ea1eec84)) +* **ssh:** bad argument when editing files over ssh ([#370](https://github.com/stevearc/oil.nvim/issues/370)) ([aa0c00c](https://github.com/stevearc/oil.nvim/commit/aa0c00c7fd51982ac476d165cd021f348cf5ea71)) +* **ssh:** config option to pass extra args to SCP ([#340](https://github.com/stevearc/oil.nvim/issues/340)) ([3abb607](https://github.com/stevearc/oil.nvim/commit/3abb6077d7d6b09f5eb794b8764223b3027f6807)) +* **ssh:** garbled output when directory has broken symlinks ([bcfc0a2](https://github.com/stevearc/oil.nvim/commit/bcfc0a2e01def5019aa14fac2fc6de20dedb6d3d)) +* support visual mode when preview window is open ([#315](https://github.com/stevearc/oil.nvim/issues/315)) ([f41d7e7](https://github.com/stevearc/oil.nvim/commit/f41d7e7cd8e4028b03c35d847b4396790ac8bb2d)) +* **windows:** convert posix paths before matching LSP watch globs ([#374](https://github.com/stevearc/oil.nvim/issues/374)) ([f630887](https://github.com/stevearc/oil.nvim/commit/f630887cd845a7341bc16488fe8aaecffe3aaa8a)) +* **windows:** file operation preview uses only backslash path separator ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([96f0983](https://github.com/stevearc/oil.nvim/commit/96f0983e754694e592d4313f583cd31eaebfa80d)) +* **windows:** navigating into drive letter root directories ([#341](https://github.com/stevearc/oil.nvim/issues/341)) ([f3a31eb](https://github.com/stevearc/oil.nvim/commit/f3a31eba24587bc038592103d8f7e64648292115)) +* **windows:** treat both backslash and frontslash as path separators ([#336](https://github.com/stevearc/oil.nvim/issues/336)) ([3b3a6b2](https://github.com/stevearc/oil.nvim/commit/3b3a6b23a120e69ddc980c9d32840ecd521fbff9)) + ## [2.8.0](https://github.com/stevearc/oil.nvim/compare/v2.7.0...v2.8.0) (2024-04-19) From 6f452e8d476c43a149794e1827f2da7f73801a59 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Thu, 16 May 2024 12:15:16 -0700 Subject: [PATCH 025/206] ci: upgrade release-please-action to v4 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ebd7266..6cec6de 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -110,7 +110,7 @@ jobs: - update_docs runs-on: ubuntu-22.04 steps: - - uses: google-github-actions/release-please-action@v3 + - uses: googleapis/release-please-action@v4 id: release with: release-type: simple From 9e3a02252dc5686f374f79f50a13cfe7547c05af Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Thu, 16 May 2024 12:24:19 -0700 Subject: [PATCH 026/206] ci: remove package-name from release-please config --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6cec6de..5861d1b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -114,7 +114,6 @@ jobs: id: release with: release-type: simple - package-name: oil.nvim - uses: actions/checkout@v4 - uses: rickstaa/action-create-tag@v1 if: ${{ steps.release.outputs.release_created }} From 06a19f77f1a1da37b675635e6f9c5b5d50bcaacd Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 17 May 2024 12:02:35 -0700 Subject: [PATCH 027/206] fix: error opening command window from oil float (#378) --- lua/oil/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 737f21c..2075c24 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -295,7 +295,7 @@ M.open_float = function(dir) desc = "Close floating oil window", group = "Oil", callback = vim.schedule_wrap(function() - if util.is_floating_win() then + if util.is_floating_win() or vim.fn.win_gettype() == "command" then return end if vim.api.nvim_win_is_valid(winid) then From 259b1fbc84734bfb74225b2c2f408dd7ed9cf474 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 21 May 2024 19:39:37 -0700 Subject: [PATCH 028/206] doc: better type annotations for API methods --- doc/api.md | 2 +- doc/oil.txt | 2 +- lua/oil/init.lua | 14 ++++++++------ scripts/generate.py | 18 +++++++++++------- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/doc/api.md b/doc/api.md index 7221036..a6f62ad 100644 --- a/doc/api.md +++ b/doc/api.md @@ -136,7 +136,7 @@ Select the entry under the cursor | Param | Type | Desc | | | -------- | ---------------------------- | -------------------------------------------------- | ---------------------------------------------------- | -| opts | `nil\|table` | | | +| opts | `nil\|oil.SelectOpts` | | | | | vertical | `boolean` | Open the buffer in a vertical split | | | horizontal | `boolean` | Open the buffer in a horizontal split | | | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | diff --git a/doc/oil.txt b/doc/oil.txt index 2c6a5d0..09e560f 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -311,7 +311,7 @@ select({opts}, {callback}) *oil.selec Select the entry under the cursor Parameters: - {opts} `nil|table` + {opts} `nil|oil.SelectOpts` {vertical} `boolean` Open the buffer in a vertical split {horizontal} `boolean` Open the buffer in a horizontal split {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 2075c24..96c3eff 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -539,13 +539,15 @@ M.open_preview = function(opts, callback) end) end +---@class (exact) oil.SelectOpts +---@field vertical boolean Open the buffer in a vertical split +---@field horizontal boolean Open the buffer in a horizontal split +---@field split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier +---@field tab boolean Open the buffer in a new tab +---@field close boolean Close the original oil buffer once selection is made + ---Select the entry under the cursor ----@param opts nil|table ---- vertical boolean Open the buffer in a vertical split ---- horizontal boolean Open the buffer in a horizontal split ---- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier ---- tab boolean Open the buffer in a new tab ---- close boolean Close the original oil buffer once selection is made +---@param opts nil|oil.SelectOpts ---@param callback nil|fun(err: nil|string) Called once all entries have been opened M.select = function(opts, callback) local cache = require("oil.cache") diff --git a/scripts/generate.py b/scripts/generate.py index 99c590b..bf2358f 100755 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -6,16 +6,18 @@ from typing import List from nvim_doc_tools import ( LuaParam, + LuaTypes, Vimdoc, VimdocSection, generate_md_toc, indent, leftright, + parse_directory, parse_functions, read_nvim_json, read_section, - render_md_api, - render_vimdoc_api, + render_md_api2, + render_vimdoc_api2, replace_section, wrap, ) @@ -37,8 +39,9 @@ def add_md_link_path(path: str, lines: List[str]) -> List[str]: def update_md_api(): api_doc = os.path.join(DOC, "api.md") - funcs = parse_functions(os.path.join(ROOT, "lua", "oil", "init.lua")) - lines = ["\n"] + render_md_api(funcs, 2) + ["\n"] + types = parse_directory(os.path.join(ROOT, "lua")) + funcs = types.files["oil/init.lua"].functions + lines = ["\n"] + render_md_api2(funcs, types, 2) + ["\n"] replace_section( api_doc, r"^$", @@ -264,7 +267,7 @@ def get_columns_vimdoc() -> "VimdocSection": section.body.extend(wrap(col.summary, 4)) section.body.append("\n") section.body.append(" Parameters:\n") - section.body.extend(format_vimdoc_params(col.params, 6)) + section.body.extend(format_vimdoc_params(col.params, LuaTypes(), 6)) section.body.append("\n") return section @@ -302,12 +305,13 @@ Windows: def generate_vimdoc(): doc = Vimdoc("oil.txt", "oil") - funcs = parse_functions(os.path.join(ROOT, "lua", "oil", "init.lua")) + types = parse_directory(os.path.join(ROOT, "lua")) + funcs = types.files["oil/init.lua"].functions doc.sections.extend( [ get_options_vimdoc(), get_options_detail_vimdoc(), - VimdocSection("API", "oil-api", render_vimdoc_api("oil", funcs)), + VimdocSection("API", "oil-api", render_vimdoc_api2("oil", funcs, types)), get_columns_vimdoc(), get_actions_vimdoc(), get_highlights_vimdoc(), From 2cb39e838e9dcd8b374f09a3a87a2e5ec9d372f6 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Wed, 22 May 2024 13:46:11 -0700 Subject: [PATCH 029/206] doc: more detailed type annotations for setup() call --- doc/api.md | 6 +-- doc/oil.txt | 2 +- .../trash/windows/powershell-trash.lua | 2 +- lua/oil/init.lua | 2 +- lua/oil/mutator/parser.lua | 2 +- lua/oil/types.lua | 42 +++++++++++++++++++ 6 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 lua/oil/types.lua diff --git a/doc/api.md b/doc/api.md index a6f62ad..521b985 100644 --- a/doc/api.md +++ b/doc/api.md @@ -165,9 +165,9 @@ If you provide your own callback function, there will be no notification for err `setup(opts)` \ Initialize oil -| Param | Type | Desc | -| ----- | ------------ | ---- | -| opts | `nil\|table` | | +| Param | Type | Desc | +| ----- | -------------------- | ---- | +| opts | `oil.setupOpts\|nil` | | diff --git a/doc/oil.txt b/doc/oil.txt index 09e560f..6485a04 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -338,7 +338,7 @@ setup({opts}) *oil.setu Initialize oil Parameters: - {opts} `nil|table` + {opts} `oil.setupOpts|nil` -------------------------------------------------------------------------------- COLUMNS *oil-columns* diff --git a/lua/oil/adapters/trash/windows/powershell-trash.lua b/lua/oil/adapters/trash/windows/powershell-trash.lua index a176493..d050bb0 100644 --- a/lua/oil/adapters/trash/windows/powershell-trash.lua +++ b/lua/oil/adapters/trash/windows/powershell-trash.lua @@ -33,7 +33,7 @@ ConvertTo-Json $data -Compress ---@type nil|oil.PowershellConnection local list_entries_powershell ----@param cb fun(err?: string, raw_entries: oil.WindowsRawEntry[]?) +---@param cb fun(err?: string, raw_entries?: oil.WindowsRawEntry[]) M.list_raw_entries = function(cb) if not list_entries_powershell then list_entries_powershell = Powershell.new(list_entries_init) diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 96c3eff..04bd1b7 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -988,7 +988,7 @@ end local _on_key_ns = 0 ---Initialize oil ----@param opts nil|table +---@param opts oil.setupOpts|nil M.setup = function(opts) local Ringbuf = require("oil.ringbuf") local config = require("oil.config") diff --git a/lua/oil/mutator/parser.lua b/lua/oil/mutator/parser.lua index 95e1c06..8304a9c 100644 --- a/lua/oil/mutator/parser.lua +++ b/lua/oil/mutator/parser.lua @@ -25,7 +25,7 @@ local FIELD_META = constants.FIELD_META ---@field type "delete" ---@field name string ---@field id integer ---- + ---@class (exact) oil.DiffChange ---@field type "change" ---@field entry_type oil.EntryType diff --git a/lua/oil/types.lua b/lua/oil/types.lua new file mode 100644 index 0000000..d7a3a0a --- /dev/null +++ b/lua/oil/types.lua @@ -0,0 +1,42 @@ +---@class (exact) oil.setupOpts +---@field default_file_explorer? boolean Oil will take over directory buffers (e.g. `vim .` or `:e src/`). Set to false if you still want to use netrw. +---@field columns? oil.ColumnSpec[] The columns to display. See :help oil-columns. +---@field buf_options? table Buffer-local options to use for oil buffers +---@field win_options? table Window-local options to use for oil buffers +---@field delete_to_trash? boolean Send deleted files to the trash instead of permanently deleting them (:help oil-trash). +---@field skip_confirm_for_simple_edits? boolean Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits). +---@field prompt_save_on_select_new_entry? boolean Selecting a new/moved/renamed file or directory will prompt you to save changes first (:help prompt_save_on_select_new_entry). +---@field cleanup_delay_ms? integer Oil will automatically delete hidden buffers after this delay. You can set the delay to false to disable cleanup entirely. Note that the cleanup process only starts when none of the oil buffers are currently displayed. +---@field lsp_file_methods? oil.LspFileMethods Configure LSP file operation integration. +---@field constrain_cursor? false|"name"|"editable" Constrain the cursor to the editable parts of the oil buffer. Set to `false` to disable, or "name" to keep it on the file names. +---@field experimental_watch_for_changes? boolean Set to true to watch the filesystem for changes and reload oil. +---@field keymaps? table +---@field use_default_keymaps? boolean Set to false to disable all of the above keymaps +---@field view_options? oil.ViewOptions Configure which files are shown and how they are shown. +---@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH +---@field git? oil.GitOptions EXPERIMENTAL support for performing file operations with git +---@field float? table Configuration for the floating window in oil.open_float +---@field preview? table Configuration for the actions floating preview window +---@field progress? table Configuration for the floating progress window +---@field ssh? table Configuration for the floating SSH window +---@field keymaps_help? table Configuration for the floating keymaps help window + +---@class (exact) oil.LspFileMethods +---@field timeout_ms? integer Time to wait for LSP file operations to complete before skipping. +---@field autosave_changes? boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers. + +---@class (exact) oil.ViewOptions +---@field show_hidden? boolean Show files and directories that start with "." +---@field is_hidden_file? fun(name: string, bufnr: integer): boolean This function defines what is considered a "hidden" file +---@field is_always_hidden? fun(name: string, bufnr: integer): boolean This function defines what will never be shown, even when `show_hidden` is set +---@field natural_order? boolean Sort file names in a more intuitive order for humans. Is less performant, so you may want to set to false if you work with large directories. +---@field sort? oil.SortSpec[] Sort order for the file list + +---@class (exact) oil.SortSpec +---@field [1] string +---@field [2] "asc"|"desc" + +---@class (exact) oil.GitOptions +---@field add? fun(path: string): boolean Return true to automatically git add a new file +---@field mv? fun(src_path: string, dest_path: string): boolean Return true to automatically git mv a moved file +---@field rm? fun(path: string): boolean Return true to automatically git rm a deleted file From 8ac4ba4e0ad5d928c837c663b9468db4d3e9eb14 Mon Sep 17 00:00:00 2001 From: csponge Date: Sat, 1 Jun 2024 15:39:14 -0400 Subject: [PATCH 030/206] Return from delete_hidden_buffers when win type is command (#394) * Return from delete_hidden_buffers when win type is command * lint: apply stylua formatting --------- Co-authored-by: Steven Arcangeli --- lua/oil/view.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/oil/view.lua b/lua/oil/view.lua index adea825..ec43353 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -212,7 +212,12 @@ end ---Delete unmodified, hidden oil buffers and if none remain, clear the cache M.delete_hidden_buffers = function() local visible_buffers, hidden_buffers = get_visible_hidden_buffers() - if not visible_buffers or not hidden_buffers or not vim.tbl_isempty(visible_buffers) then + if + not visible_buffers + or not hidden_buffers + or not vim.tbl_isempty(visible_buffers) + or vim.fn.win_gettype() == "command" + then return end for _, bufnr in ipairs(hidden_buffers) do From 15e071f203f4d0b66058ff89a2ef11a533250764 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sat, 1 Jun 2024 16:13:36 -0700 Subject: [PATCH 031/206] ci: typechecking no longer requires neodev --- .github/workflows/tests.yml | 2 +- doc/api.md | 18 +++++++++--------- doc/oil.txt | 12 ++++++------ lua/oil/actions.lua | 3 --- lua/oil/cache.lua | 6 ++++++ lua/oil/init.lua | 15 ++++++--------- lua/oil/keymap_util.lua | 4 ++-- lua/oil/view.lua | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5861d1b..08957e5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - uses: stevearc/nvim-typecheck-action@v1 + - uses: stevearc/nvim-typecheck-action@v2 with: path: lua diff --git a/doc/api.md b/doc/api.md index 521b985..40503f8 100644 --- a/doc/api.md +++ b/doc/api.md @@ -134,15 +134,15 @@ Preview the entry under the cursor in a split `select(opts, callback)` \ Select the entry under the cursor -| Param | Type | Desc | | -| -------- | ---------------------------- | -------------------------------------------------- | ---------------------------------------------------- | -| opts | `nil\|oil.SelectOpts` | | | -| | vertical | `boolean` | Open the buffer in a vertical split | -| | horizontal | `boolean` | Open the buffer in a horizontal split | -| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| | tab | `boolean` | Open the buffer in a new tab | -| | close | `boolean` | Close the original oil buffer once selection is made | -| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | | +| Param | Type | Desc | | +| -------- | ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------- | +| opts | `nil\|oil.SelectOpts` | | | +| | vertical | `nil\|boolean` | Open the buffer in a vertical split | +| | horizontal | `nil\|boolean` | Open the buffer in a horizontal split | +| | split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| | tab | `nil\|boolean` | Open the buffer in a new tab | +| | close | `nil\|boolean` | Close the original oil buffer once selection is made | +| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | | ## save(opts, cb) diff --git a/doc/oil.txt b/doc/oil.txt index 6485a04..b8ba7bf 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -312,13 +312,13 @@ select({opts}, {callback}) *oil.selec Parameters: {opts} `nil|oil.SelectOpts` - {vertical} `boolean` Open the buffer in a vertical split - {horizontal} `boolean` Open the buffer in a horizontal split - {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + {vertical} `nil|boolean` Open the buffer in a vertical split + {horizontal} `nil|boolean` Open the buffer in a horizontal split + {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` Split modifier - {tab} `boolean` Open the buffer in a new tab - {close} `boolean` Close the original oil buffer once selection is - made + {tab} `nil|boolean` Open the buffer in a new tab + {close} `nil|boolean` Close the original oil buffer once + selection is made {callback} `nil|fun(err: nil|string)` Called once all entries have been opened diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index cebd6b1..7b75505 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -1,9 +1,6 @@ local oil = require("oil") local util = require("oil.util") --- TODO remove after https://github.com/folke/neodev.nvim/pull/163 lands ----@diagnostic disable: inject-field - local M = {} M.show_help = { diff --git a/lua/oil/cache.lua b/lua/oil/cache.lua index 09ed44f..ef78246 100644 --- a/lua/oil/cache.lua +++ b/lua/oil/cache.lua @@ -124,6 +124,7 @@ end ---@return nil|oil.InternalEntry M.get_entry_by_url = function(url) local scheme, path = util.parse_url(url) + assert(path) local parent_url = scheme .. vim.fn.fnamemodify(path, ":h") local basename = vim.fn.fnamemodify(path, ":t") return M.list_url(parent_url)[basename] @@ -150,11 +151,13 @@ end M.perform_action = function(action) if action.type == "create" then local scheme, path = util.parse_url(action.url) + assert(path) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local name = vim.fn.fnamemodify(path, ":t") M.create_and_store_entry(parent_url, name, action.entry_type) elseif action.type == "delete" then local scheme, path = util.parse_url(action.url) + assert(path) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local name = vim.fn.fnamemodify(path, ":t") local entry = url_directory[parent_url][name] @@ -163,11 +166,13 @@ M.perform_action = function(action) parent_url_by_id[entry[FIELD_ID]] = nil elseif action.type == "move" then local src_scheme, src_path = util.parse_url(action.src_url) + assert(src_path) local src_parent_url = util.addslash(src_scheme .. vim.fn.fnamemodify(src_path, ":h")) local src_name = vim.fn.fnamemodify(src_path, ":t") local entry = url_directory[src_parent_url][src_name] local dest_scheme, dest_path = util.parse_url(action.dest_url) + assert(dest_path) local dest_parent_url = util.addslash(dest_scheme .. vim.fn.fnamemodify(dest_path, ":h")) local dest_name = vim.fn.fnamemodify(dest_path, ":t") @@ -185,6 +190,7 @@ M.perform_action = function(action) util.update_moved_buffers(action.entry_type, action.src_url, action.dest_url) elseif action.type == "copy" then local scheme, path = util.parse_url(action.dest_url) + assert(path) local parent_url = util.addslash(scheme .. vim.fn.fnamemodify(path, ":h")) local name = vim.fn.fnamemodify(path, ":t") M.create_and_store_entry(parent_url, name, action.entry_type) diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 04bd1b7..8e7fe10 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -19,6 +19,7 @@ local M = {} ---@field list fun(path: string, column_defs: string[], cb: fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())) Async function to list a directory. ---@field is_modifiable fun(bufnr: integer): boolean Return true if this directory is modifiable (allows for directories with read-only permissions). ---@field get_column fun(name: string): nil|oil.ColumnDefinition If the adapter has any adapter-specific columns, return them when fetched by name. +---@field get_parent? fun(bufname: string): string Get the parent url of the given buffer ---@field normalize_url fun(url: string, callback: fun(url: string)) Before oil opens a url it will be normalized. This allows for link following, path normalizing, and converting an oil file url to the actual path of a file. ---@field get_entry_path? fun(url: string, entry: oil.Entry, callback: fun(path: string)) Similar to normalize_url, but used when selecting an entry ---@field render_action? fun(action: oil.Action): string Render a mutation action for display in the preview window. Only needed if adapter is modifiable. @@ -29,10 +30,6 @@ local M = {} ---@field filter_action? fun(action: oil.Action): boolean When present, filter out actions as they are created ---@field filter_error? fun(action: oil.ParseError): boolean When present, filter out errors from parsing a buffer --- TODO remove after https://github.com/folke/neodev.nvim/pull/163 lands ----@diagnostic disable: undefined-field ----@diagnostic disable: inject-field - ---Get the entry on a specific line (1-indexed) ---@param bufnr integer ---@param lnum integer @@ -540,11 +537,11 @@ M.open_preview = function(opts, callback) end ---@class (exact) oil.SelectOpts ----@field vertical boolean Open the buffer in a vertical split ----@field horizontal boolean Open the buffer in a horizontal split ----@field split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier ----@field tab boolean Open the buffer in a new tab ----@field close boolean Close the original oil buffer once selection is made +---@field vertical? boolean Open the buffer in a vertical split +---@field horizontal? boolean Open the buffer in a horizontal split +---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier +---@field tab? boolean Open the buffer in a new tab +---@field close? boolean Close the original oil buffer once selection is made ---Select the entry under the cursor ---@param opts nil|oil.SelectOpts diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua index 095d971..988a9d8 100644 --- a/lua/oil/keymap_util.lua +++ b/lua/oil/keymap_util.lua @@ -99,8 +99,8 @@ M.show_help = function(keymaps) end vim.keymap.set("n", "q", "close", { buffer = bufnr }) vim.keymap.set("n", "", "close", { buffer = bufnr }) - vim.api.nvim_buf_set_option(bufnr, "modifiable", false) - vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") + vim.bo[bufnr].modifiable = false + vim.bo[bufnr].bufhidden = "wipe" local editor_width = vim.o.columns local editor_height = layout.get_editor_height() diff --git a/lua/oil/view.lua b/lua/oil/view.lua index ec43353..3af948c 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -331,7 +331,7 @@ M.initialize = function(bufnr) vim.b[bufnr].EditorConfig_disable = 1 session[bufnr] = session[bufnr] or {} for k, v in pairs(config.buf_options) do - vim.api.nvim_buf_set_option(bufnr, k, v) + vim.bo[bufnr][k] = v end M.set_win_options() vim.api.nvim_create_autocmd("BufHidden", { From d3a365c950be31a0d46920ce4ff6f23e78939b67 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 2 Jun 2024 17:45:22 -0700 Subject: [PATCH 032/206] doc: improve documentation for set_sort (#401) --- doc/api.md | 11 ++++++++--- doc/oil.txt | 7 ++++++- lua/oil/init.lua | 4 +++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/api.md b/doc/api.md index 40503f8..77a07da 100644 --- a/doc/api.md +++ b/doc/api.md @@ -59,9 +59,14 @@ Change the display columns for oil `set_sort(sort)` \ Change the sort order for oil -| Param | Type | Desc | -| ----- | ---------- | ---- | -| sort | `string[]` | [] | +| Param | Type | Desc | +| ----- | ---------------- | ------------------------------------------------------------------------------------- | +| sort | `oil.SortSpec[]` | List of columns plus direction. See :help oil-columns to see which ones are sortable. | + +**Examples:** +```lua +require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } }) +``` ## set_is_hidden_file(is_hidden_file) diff --git a/doc/oil.txt b/doc/oil.txt index b8ba7bf..11043ff 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -255,7 +255,12 @@ set_sort({sort}) *oil.set_sor Change the sort order for oil Parameters: - {sort} `string[]` [] + {sort} `oil.SortSpec[]` List of columns plus direction. See :help oil- + columns to see which ones are sortable. + + Examples: >lua + require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } }) +< set_is_hidden_file({is_hidden_file}) *oil.set_is_hidden_file* Change how oil determines if the file is hidden diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 8e7fe10..f025085 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -121,7 +121,9 @@ M.set_columns = function(cols) end ---Change the sort order for oil ----@param sort string[][] +---@param sort oil.SortSpec[] List of columns plus direction. See :help oil-columns to see which ones are sortable. +---@example +--- require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } }) M.set_sort = function(sort) require("oil.view").set_sort(sort) end From bbc0e67eebc15342e73b146a50d9b52e6148161b Mon Sep 17 00:00:00 2001 From: Kevin Traver Date: Mon, 3 Jun 2024 12:02:01 -0600 Subject: [PATCH 033/206] feat: add copy filename action (#391) --- doc/oil.txt | 3 +++ lua/oil/actions.lua | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/doc/oil.txt b/doc/oil.txt index 11043ff..056dc52 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -456,6 +456,9 @@ change_sort *actions.change_sor close *actions.close* Close oil and restore original buffer +copy_entry_filename *actions.copy_entry_filename* + Yank the filename of the entry under the cursor to a register + copy_entry_path *actions.copy_entry_path* Yank the filepath of the entry under the cursor to a register diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 7b75505..887f793 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -268,6 +268,17 @@ M.copy_entry_path = { end, } +M.copy_entry_filename = { + desc = "Yank the filename of the entry under the cursor to a register", + callback = function() + local entry = oil.get_cursor_entry() + if not entry then + return + end + vim.fn.setreg(vim.v.register, entry.name) + end, +} + M.open_cmdline_dir = { desc = "Open vim cmdline with current directory as an argument", callback = function() From e5312c3a801e7274fa14e6a56aa10a618fed80c3 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Wed, 5 Jun 2024 20:33:31 -0700 Subject: [PATCH 034/206] fix: hack around glob issues in LSP rename operations (#386) --- lua/oil/lsp/workspace.lua | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua index 79ab784..426aabf 100644 --- a/lua/oil/lsp/workspace.lua +++ b/lua/oil/lsp/workspace.lua @@ -72,8 +72,21 @@ local function get_matching_paths(client, filters, paths) glob = glob:gsub("/", "\\") end - ---@type nil|vim.lpeg.Pattern - local glob_pattern = vim.glob and vim.glob.to_lpeg and vim.glob.to_lpeg(glob) + ---@type string|vim.lpeg.Pattern + local glob_to_match = glob + if vim.glob and vim.glob.to_lpeg then + -- HACK around https://github.com/neovim/neovim/issues/28931 + -- find alternations and sort them by length to try to match the longest first + glob = glob:gsub("{(.*)}", function(s) + local pieces = vim.split(s, ",") + table.sort(pieces, function(a, b) + return a:len() > b:len() + end) + return "{" .. table.concat(pieces, ",") .. "}" + end) + + glob_to_match = vim.glob.to_lpeg(glob) + end local matches = pattern.matches table.insert(match_fns, function(path) local is_dir = vim.fn.isdirectory(path) == 1 @@ -84,7 +97,7 @@ local function get_matching_paths(client, filters, paths) if ignore_case then path = path:lower() end - return match_glob(glob_pattern or glob, path) + return match_glob(glob_to_match, path) end) end end From 18272aba9d00a3176a5443d50dbb4464acc167bd Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Sun, 9 Jun 2024 06:35:32 +0300 Subject: [PATCH 035/206] fix: notify when changing the current directory (#406) * feat: notify when changing the current directory * Update actions.lua --------- Co-authored-by: Ruslan Hrabovyi Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com> --- lua/oil/actions.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 887f793..1bb885e 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -102,6 +102,7 @@ local function cd(cmd) local dir = oil.get_current_dir() if dir then vim.cmd({ cmd = cmd, args = { dir } }) + vim.notify(string.format("CWD: %s", dir), vim.log.levels.INFO) else vim.notify("Cannot :cd; not in a directory", vim.log.levels.WARN) end From 96368e13e9b1aaacc570e4825b8787307f0d05e1 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Mon, 10 Jun 2024 16:44:59 -0500 Subject: [PATCH 036/206] feat: keymap actions can be parameterized --- README.md | 8 +- doc/oil.txt | 116 ++++++++++++++++++--------- lua/oil/actions.lua | 171 ++++++++++++++++++++++++++++++++++++---- lua/oil/config.lua | 8 +- lua/oil/keymap_util.lua | 24 +++++- scripts/generate.py | 60 +++++++++++++- 6 files changed, 321 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index d96ed18..fa31059 100644 --- a/README.md +++ b/README.md @@ -184,16 +184,16 @@ require("oil").setup({ keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = "actions.select_vsplit", - [""] = "actions.select_split", - [""] = "actions.select_tab", + [""] = { "actions.select_split", opts = { vertical = true } }, + [""] = { "actions.select_split", opts = { horizontal = true } }, + [""] = { "actions.select_split", opts = { tab = true } }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = "actions.tcd", + ["~"] = { "actions.cd", opts = { scope = "tab" } }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", diff --git a/doc/oil.txt b/doc/oil.txt index 056dc52..548c739 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -75,16 +75,16 @@ CONFIG *oil-confi keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = "actions.select_vsplit", - [""] = "actions.select_split", - [""] = "actions.select_tab", + [""] = { "actions.select_split", opts = { vertical = true } }, + [""] = { "actions.select_split", opts = { horizontal = true } }, + [""] = { "actions.select_split", opts = { tab = true } }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = "actions.tcd", + ["~"] = { "actions.cd", opts = { scope = "tab" } }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", @@ -435,38 +435,71 @@ birthtime *column-birthtim -------------------------------------------------------------------------------- ACTIONS *oil-actions* -These are actions that can be used in the `keymaps` section of config options. -You can also call them directly with +The `keymaps` option in `oil.setup` allow you to create mappings using all the same parameters as |vim.keymap.set|. +>lua + keymaps = { + -- Mappings can be a string + ["~"] = "edit $HOME", + -- Mappings can be a function + ["gd"] = function() + require("oil").set_columns({ "icon", "permissions", "size", "mtime" }) + end, + -- You can pass additional opts to vim.keymap.set by using + -- a table with the mapping as the first element. + ["ff"] = { + function() + require("telescope.builtin").find_files({ + cwd = require("oil").get_current_dir() + }) + end, + mode = "n", + nowait = true, + desc = "Find files in the current directory" + }, + -- Mappings that are a string starting with "actions." will be + -- one of the built-in actions, documented below. + ["`"] = "actions.tcd", + -- Some actions have parameters. These are passed in via the `opts` key. + [":"] = { + "actions.open_cmdline", + opts = { + shorten_path = true, + modify = ":h", + }, + desc = "Open the command line with the current directory as an argument", + }, + } + +Below are the actions that can be used in the `keymaps` section of config +options. You can refer to them as strings (e.g. "actions.") or you +can use the functions directly with `require("oil.actions").action_name.callback()` -add_to_loclist *actions.add_to_loclist* - Adds files in the current oil directory to the location list, keeping the - previous entries. - -add_to_qflist *actions.add_to_qflist* - Adds files in the current oil directory to the quickfix list, keeping the - previous entries. - cd *actions.cd* :cd to the current oil directory + Parameters: + {scope} `nil|"tab"|"win"` Scope of the directory change (e.g. use |:tcd| + or |:lcd|) + {silent} `boolean` Do not show a message when changing directories + change_sort *actions.change_sort* Change the sort order + Parameters: + {sort} `oil.SortSpec[]` List of columns plus direction (see + |oil.set_sort|) instead of interactive selection + close *actions.close* Close oil and restore original buffer -copy_entry_filename *actions.copy_entry_filename* - Yank the filename of the entry under the cursor to a register - -copy_entry_path *actions.copy_entry_path* - Yank the filepath of the entry under the cursor to a register - open_cmdline *actions.open_cmdline* Open vim cmdline with current entry as an argument -open_cmdline_dir *actions.open_cmdline_dir* - Open vim cmdline with current directory as an argument + Parameters: + {modify} `string` Modify the path with |fnamemodify()| using this as + the mods argument + {shorten_path} `boolean` Use relative paths when possible open_cwd *actions.open_cwd* Open oil in Neovim's current working directory @@ -493,38 +526,47 @@ preview_scroll_up *actions.preview_scroll_u refresh *actions.refresh* Refresh current directory list + Parameters: + {force} `boolean` When true, do not prompt user if they will be discarding + changes + select *actions.select* Open the entry under the cursor -select_split *actions.select_split* - Open the entry under the cursor in a horizontal split - -select_tab *actions.select_tab* - Open the entry under the cursor in a new tab - -select_vsplit *actions.select_vsplit* - Open the entry under the cursor in a vertical split - -send_to_loclist *actions.send_to_loclist* - Sends files in the current oil directory to the location list, replacing the - previous entries. + Parameters: + {close} `boolean` Close the original oil buffer once selection is + made + {horizontal} `boolean` Open the buffer in a horizontal split + {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + modifier + {tab} `boolean` Open the buffer in a new tab + {vertical} `boolean` Open the buffer in a vertical split send_to_qflist *actions.send_to_qflist* Sends files in the current oil directory to the quickfix list, replacing the previous entries. + Parameters: + {action} `"r"|"a"` Replace or add to current quickfix list (see + |setqflist-action|) + {target} `"qflist"|"loclist"` The target list to send files to + show_help *actions.show_help* Show default keymaps -tcd *actions.tcd* - :tcd to the current oil directory - toggle_hidden *actions.toggle_hidden* Toggle hidden files and directories toggle_trash *actions.toggle_trash* Jump to and from the trash for the current directory +yank_entry *actions.yank_entry* + Yank the filepath of the entry under the cursor to a register + + Parameters: + {modify} `string` Modify the path with |fnamemodify()| using this as the + mods argument + -------------------------------------------------------------------------------- HIGHLIGHTS *oil-highlights* diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 1bb885e..7fc27ce 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -4,20 +4,48 @@ local util = require("oil.util") local M = {} M.show_help = { - desc = "Show default keymaps", callback = function() local config = require("oil.config") require("oil.keymap_util").show_help(config.keymaps) end, + desc = "Show default keymaps", } M.select = { desc = "Open the entry under the cursor", - callback = oil.select, + callback = function(opts) + opts = opts or {} + local callback = opts.callback + opts.callback = nil + oil.select(opts, callback) + end, + parameters = { + vertical = { + type = "boolean", + desc = "Open the buffer in a vertical split", + }, + horizontal = { + type = "boolean", + desc = "Open the buffer in a horizontal split", + }, + split = { + type = '"aboveleft"|"belowright"|"topleft"|"botright"', + desc = "Split modifier", + }, + tab = { + type = "boolean", + desc = "Open the buffer in a new tab", + }, + close = { + type = "boolean", + desc = "Close the original oil buffer once selection is made", + }, + }, } M.select_vsplit = { desc = "Open the entry under the cursor in a vertical split", + deprecated = true, callback = function() oil.select({ vertical = true }) end, @@ -25,6 +53,7 @@ M.select_vsplit = { M.select_split = { desc = "Open the entry under the cursor in a horizontal split", + deprecated = true, callback = function() oil.select({ horizontal = true }) end, @@ -32,6 +61,7 @@ M.select_split = { M.select_tab = { desc = "Open the entry under the cursor in a new tab", + deprecated = true, callback = function() oil.select({ tab = true }) end, @@ -98,11 +128,14 @@ M.close = { } ---@param cmd string -local function cd(cmd) +---@param silent? boolean +local function cd(cmd, silent) local dir = oil.get_current_dir() if dir then vim.cmd({ cmd = cmd, args = { dir } }) - vim.notify(string.format("CWD: %s", dir), vim.log.levels.INFO) + if not silent then + vim.notify(string.format("CWD: %s", dir), vim.log.levels.INFO) + end else vim.notify("Cannot :cd; not in a directory", vim.log.levels.WARN) end @@ -110,13 +143,31 @@ end M.cd = { desc = ":cd to the current oil directory", - callback = function() - cd("cd") + callback = function(opts) + opts = opts or {} + local cmd = "cd" + if opts.scope == "tab" then + cmd = "tcd" + elseif opts.scope == "win" then + cmd = "lcd" + end + cd(cmd, opts.silent) end, + parameters = { + scope = { + type = 'nil|"tab"|"win"', + desc = "Scope of the directory change (e.g. use |:tcd| or |:lcd|)", + }, + silent = { + type = "boolean", + desc = "Do not show a message when changing directories", + }, + }, } M.tcd = { desc = ":tcd to the current oil directory", + deprecated = true, callback = function() cd("tcd") end, @@ -200,8 +251,12 @@ M.open_external = { return end local path = dir .. entry.name - -- TODO use vim.ui.open once this is resolved - -- https://github.com/neovim/neovim/issues/24567 + + if vim.ui.open then + vim.ui.open(path) + return + end + local cmd, err = get_open_cmd(path) if not cmd then vim.notify(string.format("Could not open %s: %s", path, err), vim.log.levels.ERROR) @@ -214,8 +269,9 @@ M.open_external = { M.refresh = { desc = "Refresh current directory list", - callback = function() - if vim.bo.modified then + callback = function(opts) + opts = opts or {} + if vim.bo.modified and not opts.force then local ok, choice = pcall(vim.fn.confirm, "Discard changes?", "No\nYes") if not ok or choice ~= 2 then return @@ -226,6 +282,12 @@ M.refresh = { -- :h CTRL-L-default vim.cmd.nohlsearch() end, + parameters = { + force = { + desc = "When true, do not prompt user if they will be discarding changes", + type = "boolean", + }, + }, } local function open_cmdline_with_path(path) @@ -236,7 +298,10 @@ end M.open_cmdline = { desc = "Open vim cmdline with current entry as an argument", - callback = function() + callback = function(opts) + opts = vim.tbl_deep_extend("keep", opts or {}, { + shorten_path = true, + }) local config = require("oil.config") local fs = require("oil.fs") local entry = oil.get_cursor_entry() @@ -252,13 +317,53 @@ M.open_cmdline = { if not adapter or not path or adapter.name ~= "files" then return end - local fullpath = fs.shorten_path(fs.posix_to_os_path(path) .. entry.name) + local fullpath = fs.posix_to_os_path(path) .. entry.name + if opts.modify then + fullpath = vim.fn.fnamemodify(fullpath, opts.modify) + end + if opts.shorten_path then + fullpath = fs.shorten_path(fullpath) + end open_cmdline_with_path(fullpath) end, + parameters = { + modify = { + desc = "Modify the path with |fnamemodify()| using this as the mods argument", + type = "string", + }, + shorten_path = { + desc = "Use relative paths when possible", + type = "boolean", + }, + }, +} + +M.yank_entry = { + desc = "Yank the filepath of the entry under the cursor to a register", + callback = function(opts) + opts = opts or {} + local entry = oil.get_cursor_entry() + local dir = oil.get_current_dir() + if not entry or not dir then + return + end + local path = dir .. entry.name + if opts.modify then + path = vim.fn.fnamemodify(path, opts.modify) + end + vim.fn.setreg(vim.v.register, path) + end, + parameters = { + modify = { + desc = "Modify the path with |fnamemodify()| using this as the mods argument", + type = "string", + }, + }, } M.copy_entry_path = { desc = "Yank the filepath of the entry under the cursor to a register", + deprecated = true, callback = function() local entry = oil.get_cursor_entry() local dir = oil.get_current_dir() @@ -271,6 +376,7 @@ M.copy_entry_path = { M.copy_entry_filename = { desc = "Yank the filename of the entry under the cursor to a register", + deprecated = true, callback = function() local entry = oil.get_cursor_entry() if not entry then @@ -282,6 +388,7 @@ M.copy_entry_filename = { M.open_cmdline_dir = { desc = "Open vim cmdline with current directory as an argument", + deprecated = true, callback = function() local fs = require("oil.fs") local dir = oil.get_current_dir() @@ -293,7 +400,14 @@ M.open_cmdline_dir = { M.change_sort = { desc = "Change the sort order", - callback = function() + callback = function(opts) + opts = opts or {} + + if opts.sort then + oil.set_sort(opts.sort) + return + end + local sort_cols = { "name", "size", "atime", "mtime", "ctime", "birthtime" } vim.ui.select(sort_cols, { prompt = "Sort by", kind = "oil_sort_col" }, function(col) if not col then @@ -315,6 +429,12 @@ M.change_sort = { ) end) end, + parameters = { + sort = { + type = "oil.SortSpec[]", + desc = "List of columns plus direction (see |oil.set_sort|) instead of interactive selection", + }, + }, } M.toggle_trash = { @@ -348,16 +468,31 @@ M.toggle_trash = { M.send_to_qflist = { desc = "Sends files in the current oil directory to the quickfix list, replacing the previous entries.", - callback = function() - util.send_to_quickfix({ + callback = function(opts) + opts = vim.tbl_deep_extend("keep", opts or {}, { target = "qflist", - mode = "r", + action = "r", + }) + util.send_to_quickfix({ + target = opts.target, + action = opts.action, }) end, + parameters = { + target = { + type = '"qflist"|"loclist"', + desc = "The target list to send files to", + }, + action = { + type = '"r"|"a"', + desc = "Replace or add to current quickfix list (see |setqflist-action|)", + }, + }, } M.add_to_qflist = { desc = "Adds files in the current oil directory to the quickfix list, keeping the previous entries.", + deprecated = true, callback = function() util.send_to_quickfix({ target = "qflist", @@ -368,6 +503,7 @@ M.add_to_qflist = { M.send_to_loclist = { desc = "Sends files in the current oil directory to the location list, replacing the previous entries.", + deprecated = true, callback = function() util.send_to_quickfix({ target = "loclist", @@ -378,6 +514,7 @@ M.send_to_loclist = { M.add_to_loclist = { desc = "Adds files in the current oil directory to the location list, keeping the previous entries.", + deprecated = true, callback = function() util.send_to_quickfix({ target = "loclist", @@ -395,6 +532,8 @@ M._get_actions = function() table.insert(ret, { name = name, desc = action.desc, + deprecated = action.deprecated, + parameters = action.parameters, }) end end diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 1defbe1..af19c05 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -58,16 +58,16 @@ local default_config = { keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = "actions.select_vsplit", - [""] = "actions.select_split", - [""] = "actions.select_tab", + [""] = { "actions.select_split", opts = { vertical = true } }, + [""] = { "actions.select_split", opts = { horizontal = true } }, + [""] = { "actions.select_split", opts = { tab = true } }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = "actions.tcd", + ["~"] = { "actions.cd", opts = { scope = "tab" } }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua index 988a9d8..834ce93 100644 --- a/lua/oil/keymap_util.lua +++ b/lua/oil/keymap_util.lua @@ -10,10 +10,14 @@ local M = {} ---@return string|nil mode local function resolve(rhs) if type(rhs) == "string" and vim.startswith(rhs, "actions.") then - return resolve(actions[vim.split(rhs, ".", { plain = true })[2]]) + local action_name = vim.split(rhs, ".", { plain = true })[2] + local action = actions[action_name] + assert(action, "Unknown action name: " .. action_name) + return resolve(action) elseif type(rhs) == "table" then local opts = vim.deepcopy(rhs) - local callback = opts.callback + -- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap + local callback = resolve(opts.callback or opts[1]) local mode = opts.mode if type(rhs.callback) == "string" then local action_opts, action_mode @@ -21,8 +25,24 @@ local function resolve(rhs) opts = vim.tbl_extend("keep", opts, action_opts) mode = mode or action_mode end + + -- remove all the keys that we can't pass as options to `vim.keymap.set` opts.callback = nil opts.mode = nil + opts[1] = nil + opts.deprecated = nil + opts.parameters = nil + + if opts.opts and type(callback) == "function" then + local callback_args = opts.opts + opts.opts = nil + local orig_callback = callback + callback = function() + ---@diagnostic disable-next-line: redundant-parameter + orig_callback(callback_args) + end + end + return callback, opts, mode else return rhs, {} diff --git a/scripts/generate.py b/scripts/generate.py index bf2358f..7202dc8 100755 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -2,7 +2,7 @@ import os import os.path import re from dataclasses import dataclass, field -from typing import List +from typing import Any, Dict, List from nvim_doc_tools import ( LuaParam, @@ -13,7 +13,6 @@ from nvim_doc_tools import ( indent, leftright, parse_directory, - parse_functions, read_nvim_json, read_section, render_md_api2, @@ -228,21 +227,76 @@ def get_highlights_vimdoc() -> "VimdocSection": return section +def load_params(params: Dict[str, Any]) -> List[LuaParam]: + ret = [] + for name, data in sorted(params.items()): + ret.append(LuaParam(name, data["type"], data["desc"])) + return ret + + def get_actions_vimdoc() -> "VimdocSection": section = VimdocSection("Actions", "oil-actions", ["\n"]) + section.body.append( + """The `keymaps` option in `oil.setup` allow you to create mappings using all the same parameters as |vim.keymap.set|. +>lua + keymaps = { + -- Mappings can be a string + ["~"] = "edit $HOME", + -- Mappings can be a function + ["gd"] = function() + require("oil").set_columns({ "icon", "permissions", "size", "mtime" }) + end, + -- You can pass additional opts to vim.keymap.set by using + -- a table with the mapping as the first element. + ["ff"] = { + function() + require("telescope.builtin").find_files({ + cwd = require("oil").get_current_dir() + }) + end, + mode = "n", + nowait = true, + desc = "Find files in the current directory" + }, + -- Mappings that are a string starting with "actions." will be + -- one of the built-in actions, documented below. + ["`"] = "actions.tcd", + -- Some actions have parameters. These are passed in via the `opts` key. + [":"] = { + "actions.open_cmdline", + opts = { + shorten_path = true, + modify = ":h", + }, + desc = "Open the command line with the current directory as an argument", + }, + } +""" + ) + section.body.append("\n") section.body.extend( wrap( - """These are actions that can be used in the `keymaps` section of config options. You can also call them directly with `require("oil.actions").action_name.callback()`""" + """Below are the actions that can be used in the `keymaps` section of config options. You can refer to them as strings (e.g. "actions.") or you can use the functions directly with `require("oil.actions").action_name.callback()`""" ) ) section.body.append("\n") actions = read_nvim_json('require("oil.actions")._get_actions()') actions.sort(key=lambda a: a["name"]) for action in actions: + if action.get("deprecated"): + continue name = action["name"] desc = action["desc"] section.body.append(leftright(name, f"*actions.{name}*")) section.body.extend(wrap(desc, 4)) + params = action.get("parameters") + if params: + section.body.append("\n") + section.body.append(" Parameters:\n") + section.body.extend( + format_vimdoc_params(load_params(params), LuaTypes(), 6) + ) + section.body.append("\n") return section From a62ec258d1c99d354b256a2ec39733ad9109d808 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Mon, 10 Jun 2024 16:45:03 -0500 Subject: [PATCH 037/206] refactor: Neovim 0.11 won't need the glob ordering hack --- lua/oil/lsp/workspace.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua index 426aabf..85c8b5b 100644 --- a/lua/oil/lsp/workspace.lua +++ b/lua/oil/lsp/workspace.lua @@ -77,13 +77,15 @@ local function get_matching_paths(client, filters, paths) if vim.glob and vim.glob.to_lpeg then -- HACK around https://github.com/neovim/neovim/issues/28931 -- find alternations and sort them by length to try to match the longest first - glob = glob:gsub("{(.*)}", function(s) - local pieces = vim.split(s, ",") - table.sort(pieces, function(a, b) - return a:len() > b:len() + if vim.has("nvim-0.11") == 0 then + glob = glob:gsub("{(.*)}", function(s) + local pieces = vim.split(s, ",") + table.sort(pieces, function(a, b) + return a:len() > b:len() + end) + return "{" .. table.concat(pieces, ",") .. "}" end) - return "{" .. table.concat(pieces, ",") .. "}" - end) + end glob_to_match = vim.glob.to_lpeg(glob) end From e5eb20e88fc03bf89f371032de77f176158b41d3 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 11 Jun 2024 06:21:33 -0500 Subject: [PATCH 038/206] fix: change unknown action name from error to notification --- lua/oil/keymap_util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua index 834ce93..85ff23e 100644 --- a/lua/oil/keymap_util.lua +++ b/lua/oil/keymap_util.lua @@ -12,7 +12,7 @@ local function resolve(rhs) if type(rhs) == "string" and vim.startswith(rhs, "actions.") then local action_name = vim.split(rhs, ".", { plain = true })[2] local action = actions[action_name] - assert(action, "Unknown action name: " .. action_name) + vim.notify(action, "[oil.nvim] Unknown action name: " .. action_name, vim.log.levels.ERROR) return resolve(action) elseif type(rhs) == "table" then local opts = vim.deepcopy(rhs) From 61f1967222365474c6cf7953c569cc94dbcc7acd Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Tue, 11 Jun 2024 21:23:04 +1000 Subject: [PATCH 039/206] fix: throw error on vim.has call within the lsp/workspace.lua (#411) --- lua/oil/lsp/workspace.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua index 85c8b5b..ac8e180 100644 --- a/lua/oil/lsp/workspace.lua +++ b/lua/oil/lsp/workspace.lua @@ -77,7 +77,7 @@ local function get_matching_paths(client, filters, paths) if vim.glob and vim.glob.to_lpeg then -- HACK around https://github.com/neovim/neovim/issues/28931 -- find alternations and sort them by length to try to match the longest first - if vim.has("nvim-0.11") == 0 then + if vim.fn.has("nvim-0.11") == 0 then glob = glob:gsub("{(.*)}", function(s) local pieces = vim.split(s, ",") table.sort(pieces, function(a, b) From 76bfc25520e4edc98d089d023b4ed06013639849 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 11 Jun 2024 06:27:39 -0500 Subject: [PATCH 040/206] fix: vim.notify call error --- lua/oil/keymap_util.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua index 85ff23e..b94581c 100644 --- a/lua/oil/keymap_util.lua +++ b/lua/oil/keymap_util.lua @@ -12,7 +12,9 @@ local function resolve(rhs) if type(rhs) == "string" and vim.startswith(rhs, "actions.") then local action_name = vim.split(rhs, ".", { plain = true })[2] local action = actions[action_name] - vim.notify(action, "[oil.nvim] Unknown action name: " .. action_name, vim.log.levels.ERROR) + if not action then + vim.notify("[oil.nvim] Unknown action name: " .. action_name, vim.log.levels.ERROR) + end return resolve(action) elseif type(rhs) == "table" then local opts = vim.deepcopy(rhs) From c82b26eb4ba35c0eb7ec38d88dd400597fb34883 Mon Sep 17 00:00:00 2001 From: k14lb3 <76220140+k14lb3@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:17:42 +0800 Subject: [PATCH 041/206] fix: incorrect default config actions (#414) --- README.md | 6 +++--- doc/oil.txt | 6 +++--- lua/oil/config.lua | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fa31059..67b2416 100644 --- a/README.md +++ b/README.md @@ -184,9 +184,9 @@ require("oil").setup({ keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select_split", opts = { vertical = true } }, - [""] = { "actions.select_split", opts = { horizontal = true } }, - [""] = { "actions.select_split", opts = { tab = true } }, + [""] = { "actions.select", opts = { vertical = true } }, + [""] = { "actions.select", opts = { horizontal = true } }, + [""] = { "actions.select", opts = { tab = true } }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", diff --git a/doc/oil.txt b/doc/oil.txt index 548c739..b20dbe9 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -75,9 +75,9 @@ CONFIG *oil-confi keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select_split", opts = { vertical = true } }, - [""] = { "actions.select_split", opts = { horizontal = true } }, - [""] = { "actions.select_split", opts = { tab = true } }, + [""] = { "actions.select", opts = { vertical = true } }, + [""] = { "actions.select", opts = { horizontal = true } }, + [""] = { "actions.select", opts = { tab = true } }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", diff --git a/lua/oil/config.lua b/lua/oil/config.lua index af19c05..563ed0f 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -58,9 +58,9 @@ local default_config = { keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select_split", opts = { vertical = true } }, - [""] = { "actions.select_split", opts = { horizontal = true } }, - [""] = { "actions.select_split", opts = { tab = true } }, + [""] = { "actions.select", opts = { vertical = true } }, + [""] = { "actions.select", opts = { horizontal = true } }, + [""] = { "actions.select", opts = { tab = true } }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", From ca8b62fca5ca2dfdb38608e4a7f76a6fe81b18bc Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Thu, 13 Jun 2024 15:42:57 -0400 Subject: [PATCH 042/206] doc: restore description for keymaps --- README.md | 8 ++++---- doc/oil.txt | 8 ++++---- lua/oil/config.lua | 10 ++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 67b2416..5b4022f 100644 --- a/README.md +++ b/README.md @@ -184,16 +184,16 @@ require("oil").setup({ keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select", opts = { vertical = true } }, - [""] = { "actions.select", opts = { horizontal = true } }, - [""] = { "actions.select", opts = { tab = true } }, + [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" }, + [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" }, + [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in a horizontal split" }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = { "actions.cd", opts = { scope = "tab" } }, + ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", diff --git a/doc/oil.txt b/doc/oil.txt index b20dbe9..78b9209 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -75,16 +75,16 @@ CONFIG *oil-confi keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select", opts = { vertical = true } }, - [""] = { "actions.select", opts = { horizontal = true } }, - [""] = { "actions.select", opts = { tab = true } }, + [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" }, + [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" }, + [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in a horizontal split" }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = { "actions.cd", opts = { scope = "tab" } }, + ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 563ed0f..615a436 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -1,3 +1,5 @@ +--stylua: ignore + local default_config = { -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) -- Set to false if you still want to use netrw. @@ -58,16 +60,16 @@ local default_config = { keymaps = { ["g?"] = "actions.show_help", [""] = "actions.select", - [""] = { "actions.select", opts = { vertical = true } }, - [""] = { "actions.select", opts = { horizontal = true } }, - [""] = { "actions.select", opts = { tab = true } }, + [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" }, + [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" }, + [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in a horizontal split" }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = { "actions.cd", opts = { scope = "tab" } }, + ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", From b77ed915ab1e53720a6283702816cea2695a2638 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Thu, 13 Jun 2024 16:17:59 -0400 Subject: [PATCH 043/206] doc: add recipes --- README.md | 6 ++++ doc/recipes.md | 77 +++++++++++++++++++++++++++++++++++++++++++++ scripts/generate.py | 23 ++++++++++++-- 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 doc/recipes.md diff --git a/README.md b/README.md index 5b4022f..bfb0b31 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94 - [Quick start](#quick-start) - [Options](#options) - [Adapters](#adapters) +- [Recipes](#recipes) - [API](#api) - [FAQ](#faq) @@ -321,6 +322,11 @@ This may look familiar. In fact, this is the same url format that netrw uses. Note that at the moment the ssh adapter does not support Windows machines, and it requires the server to have a `/bin/sh` binary as well as standard unix commands (`ls`, `rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`). +## Recipes + +- [Toggle file detail view](doc/recipes.md#toggle-file-detail-view) +- [Hide gitignored files](doc/recipes.md#hide-gitignored-files) + ## API diff --git a/doc/recipes.md b/doc/recipes.md new file mode 100644 index 0000000..3a78a1f --- /dev/null +++ b/doc/recipes.md @@ -0,0 +1,77 @@ +# Recipes + +Have a cool recipe to share? Open a pull request and add it to this doc! + + + +- [Toggle file detail view](#toggle-file-detail-view) +- [Hide gitignored files](#hide-gitignored-files) + + + +## Toggle file detail view + +```lua +local detail = false +require("oil").setup({ + keymaps = { + ["gd"] = { + desc = "Toggle file detail view", + callback = function() + detail = not detail + if detail then + require("oil").set_columns({ "icon", "permissions", "size", "mtime" }) + else + require("oil").set_columns({ "icon" }) + end + end, + }, + }, +}) +``` + +## Hide gitignored files + +```lua +local git_ignored = setmetatable({}, { + __index = function(self, key) + local proc = vim.system( + { "git", "ls-files", "--ignored", "--exclude-standard", "--others", "--directory" }, + { + cwd = key, + text = true, + } + ) + local result = proc:wait() + local ret = {} + if result.code == 0 then + for line in vim.gsplit(result.stdout, "\n", { plain = true, trimempty = true }) do + -- Remove trailing slash + line = line:gsub("/$", "") + table.insert(ret, line) + end + end + + rawset(self, key, ret) + return ret + end, +}) + +require("oil").setup({ + view_options = { + is_hidden_file = function(name, _) + -- dotfiles are always considered hidden + if vim.startswith(name, ".") then + return true + end + local dir = require("oil").get_current_dir() + -- if no local directory (e.g. for ssh connections), always show + if not dir then + return false + end + -- Check if file is gitignored + return vim.list_contains(git_ignored[dir], name) + end, + }, +}) +``` diff --git a/scripts/generate.py b/scripts/generate.py index 7202dc8..8875e9f 100755 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -63,10 +63,25 @@ def update_md_api(): ) -def update_readme_toc(): - toc = ["\n"] + generate_md_toc(README, max_level=1) + ["\n"] +def update_readme(): + def get_toc(filename: str) -> List[str]: + subtoc = generate_md_toc(os.path.join(DOC, filename)) + return add_md_link_path("doc/" + filename, subtoc) + + recipes_toc = get_toc("recipes.md") + replace_section( README, + r"^## Recipes$", + r"^#", + ["\n"] + recipes_toc + ["\n"], + ) + + +def update_md_toc(filename: str, max_level: int = 99): + toc = ["\n"] + generate_md_toc(filename, max_level) + ["\n"] + replace_section( + filename, r"^$", r"^$", toc, @@ -381,5 +396,7 @@ def main() -> None: """Update the README""" update_config_options() update_md_api() - update_readme_toc() + update_md_toc(README, max_level=1) + update_md_toc(os.path.join(DOC, "recipes.md")) + update_readme() generate_vimdoc() From 64a3a555b40d96faae488ed6cf5d0f8b38520891 Mon Sep 17 00:00:00 2001 From: HyBer <59885141+BBboy01@users.noreply.github.com> Date: Sun, 16 Jun 2024 10:18:59 +0800 Subject: [PATCH 044/206] doc: update desc (#419) --- README.md | 2 +- doc/oil.txt | 2 +- lua/oil/config.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bfb0b31..1929c38 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ require("oil").setup({ [""] = "actions.select", [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" }, [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" }, - [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in a horizontal split" }, + [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", diff --git a/doc/oil.txt b/doc/oil.txt index 78b9209..8e55183 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -77,7 +77,7 @@ CONFIG *oil-confi [""] = "actions.select", [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" }, [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" }, - [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in a horizontal split" }, + [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 615a436..1b6f085 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -62,7 +62,7 @@ local default_config = { [""] = "actions.select", [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" }, [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" }, - [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in a horizontal split" }, + [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" }, [""] = "actions.preview", [""] = "actions.close", [""] = "actions.refresh", From 0883b109a7203d9ff2559db4a345407172cb29fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:21:43 -0400 Subject: [PATCH 045/206] chore(master): release 2.10.0 (#379) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d4660..280d010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [2.10.0](https://github.com/stevearc/oil.nvim/compare/v2.9.0...v2.10.0) (2024-06-16) + + +### Features + +* add copy filename action ([#391](https://github.com/stevearc/oil.nvim/issues/391)) ([bbc0e67](https://github.com/stevearc/oil.nvim/commit/bbc0e67eebc15342e73b146a50d9b52e6148161b)) +* keymap actions can be parameterized ([96368e1](https://github.com/stevearc/oil.nvim/commit/96368e13e9b1aaacc570e4825b8787307f0d05e1)) + + +### Bug Fixes + +* change unknown action name from error to notification ([e5eb20e](https://github.com/stevearc/oil.nvim/commit/e5eb20e88fc03bf89f371032de77f176158b41d3)) +* error opening command window from oil float ([#378](https://github.com/stevearc/oil.nvim/issues/378)) ([06a19f7](https://github.com/stevearc/oil.nvim/commit/06a19f77f1a1da37b675635e6f9c5b5d50bcaacd)) +* hack around glob issues in LSP rename operations ([#386](https://github.com/stevearc/oil.nvim/issues/386)) ([e5312c3](https://github.com/stevearc/oil.nvim/commit/e5312c3a801e7274fa14e6a56aa10a618fed80c3)) +* incorrect default config actions ([#414](https://github.com/stevearc/oil.nvim/issues/414)) ([c82b26e](https://github.com/stevearc/oil.nvim/commit/c82b26eb4ba35c0eb7ec38d88dd400597fb34883)) +* notify when changing the current directory ([#406](https://github.com/stevearc/oil.nvim/issues/406)) ([18272ab](https://github.com/stevearc/oil.nvim/commit/18272aba9d00a3176a5443d50dbb4464acc167bd)) +* throw error on vim.has call within the lsp/workspace.lua ([#411](https://github.com/stevearc/oil.nvim/issues/411)) ([61f1967](https://github.com/stevearc/oil.nvim/commit/61f1967222365474c6cf7953c569cc94dbcc7acd)) +* vim.notify call error ([76bfc25](https://github.com/stevearc/oil.nvim/commit/76bfc25520e4edc98d089d023b4ed06013639849)) + ## [2.9.0](https://github.com/stevearc/oil.nvim/compare/v2.8.0...v2.9.0) (2024-05-16) From 59b3dab6f79e147a0d694ee72c26ae883d323340 Mon Sep 17 00:00:00 2001 From: Philipp Oeschger <49947694+PhilippOesch@users.noreply.github.com> Date: Thu, 20 Jun 2024 03:23:30 +0200 Subject: [PATCH 046/206] feat: support preview from floating window (#403) * implement floating window * reset width on closing window * use gap from new config parameter * use minimal style for preview in floating * lower z-index * add configuration of preview position in floating window * fix in verions earlier than nvim 0.10 * close preview on opening floating window Close the any existing preview because otherwise strange errors happen when the preview is open and the floating window is opened at the same time. * reset formatting changes * remove empty line * change z-index of preview window to floating window z-index * add configurations to oil.txt * formatting * add auto configuration * update oil doc * refactor: move logic into layout.lua and eliminate flicker * fix: floating preview window title is file name * doc: clarify default_file_explorer * refactor: don't need a preview_gap option * refactor: only find preview win in current tabpage --------- Co-authored-by: Steven Arcangeli --- README.md | 4 +- doc/oil.txt | 4 +- lua/oil/actions.lua | 5 ++ lua/oil/config.lua | 4 +- lua/oil/init.lua | 109 ++++++++++++++++++++++++++------------------ lua/oil/layout.lua | 83 +++++++++++++++++++++++++++++++++ 6 files changed, 162 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 1929c38..524657d 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ You can open a directory with `:edit ` or `:Oil `. To open oil in a ```lua require("oil").setup({ -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) - -- Set to false if you still want to use netrw. + -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories. default_file_explorer = true, -- Id is automatically added at the beginning, and name at the end -- See :help oil-columns @@ -248,6 +248,8 @@ require("oil").setup({ win_options = { winblend = 0, }, + -- preview_split: Split direction: "auto", "left", "right", "above", "below". + preview_split = "auto", -- This is the config that will be passed to nvim_open_win. -- Change values here to customize the layout override = function(conf) diff --git a/doc/oil.txt b/doc/oil.txt index 8e55183..a10cd9f 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -17,7 +17,7 @@ CONFIG *oil-confi >lua require("oil").setup({ -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) - -- Set to false if you still want to use netrw. + -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories. default_file_explorer = true, -- Id is automatically added at the beginning, and name at the end -- See :help oil-columns @@ -138,6 +138,8 @@ CONFIG *oil-confi win_options = { winblend = 0, }, + -- preview_split: Split direction: "auto", "left", "right", "above", "below". + preview_split = "auto", -- This is the config that will be passed to nvim_open_win. -- Change values here to customize the layout override = function(conf) diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 7fc27ce..6a2a5ff 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -80,6 +80,11 @@ M.preview = { local cur_id = vim.w[winid].oil_entry_id if entry.id == cur_id then vim.api.nvim_win_close(winid, true) + if util.is_floating_win() then + local layout = require("oil.layout") + local win_opts = layout.get_fullscreen_win_opts() + vim.api.nvim_win_set_config(0, win_opts) + end return end end diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 1b6f085..e1dd950 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -2,7 +2,7 @@ local default_config = { -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`) - -- Set to false if you still want to use netrw. + -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories. default_file_explorer = true, -- Id is automatically added at the beginning, and name at the end -- See :help oil-columns @@ -123,6 +123,8 @@ local default_config = { win_options = { winblend = 0, }, + -- preview_split: Split direction: "auto", "left", "right", "above", "below". + preview_split = "auto", -- This is the config that will be passed to nvim_open_win. -- Change values here to customize the layout override = function(conf) diff --git a/lua/oil/init.lua b/lua/oil/init.lua index f025085..7bcb3bb 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -244,6 +244,7 @@ M.open_float = function(dir) local layout = require("oil.layout") local util = require("oil.util") local view = require("oil.view") + local parent_url, basename = M.get_url_for_path(dir) if not parent_url then return @@ -254,31 +255,7 @@ M.open_float = function(dir) local bufnr = vim.api.nvim_create_buf(false, true) vim.bo[bufnr].bufhidden = "wipe" - local total_width = vim.o.columns - local total_height = layout.get_editor_height() - local width = total_width - 2 * config.float.padding - if config.float.border ~= "none" then - width = width - 2 -- The border consumes 1 col on each side - end - if config.float.max_width > 0 then - width = math.min(width, config.float.max_width) - end - local height = total_height - 2 * config.float.padding - if config.float.max_height > 0 then - height = math.min(height, config.float.max_height) - end - local row = math.floor((total_height - height) / 2) - local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width - local win_opts = { - relative = "editor", - width = width, - height = height, - row = row, - col = col, - border = config.float.border, - zindex = 45, - } - win_opts = config.float.override(win_opts) or win_opts + local win_opts = layout.get_fullscreen_win_opts() local original_winid = vim.api.nvim_get_current_win() local winid = vim.api.nvim_open_win(bufnr, true, win_opts) @@ -332,12 +309,13 @@ M.open_float = function(dir) if not vim.api.nvim_win_is_valid(winid) or vim.api.nvim_win_get_buf(winid) ~= winbuf then return end + local cur_win_opts = vim.api.nvim_win_get_config(winid) vim.api.nvim_win_set_config(winid, { relative = "editor", - row = win_opts.row, - col = win_opts.col, - width = win_opts.width, - height = win_opts.height, + row = cur_win_opts.row, + col = cur_win_opts.col, + width = cur_win_opts.width, + height = cur_win_opts.height, title = get_title(), }) end, @@ -444,6 +422,8 @@ end --- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier M.open_preview = function(opts, callback) opts = opts or {} + local config = require("oil.config") + local layout = require("oil.layout") local util = require("oil.util") local function finish(err) @@ -465,18 +445,59 @@ M.open_preview = function(opts, callback) opts.split = vim.o.splitright and "belowright" or "aboveleft" end end - if util.is_floating_win() then - return finish("oil preview doesn't work in a floating window") - end + + local preview_win = util.get_preview_win() + local prev_win = vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_get_current_buf() local entry = M.get_cursor_entry() if not entry then return finish("Could not find entry under cursor") end + local entry_title = entry.name + if entry.type == "directory" then + entry_title = entry_title .. "/" + end - local preview_win = util.get_preview_win() - local prev_win = vim.api.nvim_get_current_win() - local bufnr = vim.api.nvim_get_current_buf() + if util.is_floating_win() then + if preview_win == nil then + local root_win_opts, preview_win_opts = + layout.split_window(0, config.float.preview_split, config.float.padding) + + local win_opts_oil = { + relative = "editor", + width = root_win_opts.width, + height = root_win_opts.height, + row = root_win_opts.row, + col = root_win_opts.col, + border = config.float.border, + zindex = 45, + } + vim.api.nvim_win_set_config(0, win_opts_oil) + local win_opts = { + relative = "editor", + width = preview_win_opts.width, + height = preview_win_opts.height, + row = preview_win_opts.row, + col = preview_win_opts.col, + border = config.float.border, + zindex = 45, + focusable = false, + noautocmd = true, + style = "minimal", + } + + if vim.fn.has("nvim-0.9") == 1 then + win_opts.title = entry_title + end + + preview_win = vim.api.nvim_open_win(bufnr, true, win_opts) + vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = preview_win }) + vim.api.nvim_set_current_win(prev_win) + elseif vim.fn.has("nvim-0.9") == 1 then + vim.api.nvim_win_set_config(preview_win, { title = entry_title }) + end + end local cmd = preview_win and "buffer" or "sbuffer" local mods = { @@ -485,7 +506,6 @@ M.open_preview = function(opts, callback) split = opts.split, } - local is_visual_mode = util.is_visual_mode() -- HACK Switching windows takes us out of visual mode. -- Switching with nvim_set_current_win causes the previous visual selection (as used by `gv`) to -- not get set properly. So we have to switch windows this way instead. @@ -494,15 +514,16 @@ M.open_preview = function(opts, callback) vim.cmd.wincmd({ args = { "w" }, count = winnr }) end - if preview_win then - if is_visual_mode then - hack_set_win(preview_win) - else - vim.api.nvim_set_current_win(preview_win) - end - end - util.get_edit_path(bufnr, entry, function(normalized_url) + local is_visual_mode = util.is_visual_mode() + if preview_win then + if is_visual_mode then + hack_set_win(preview_win) + else + vim.api.nvim_set_current_win(preview_win) + end + end + local filebufnr = vim.fn.bufadd(normalized_url) local entry_is_file = not vim.endswith(normalized_url, "/") diff --git a/lua/oil/layout.lua b/lua/oil/layout.lua index b4e3fed..1ed257a 100644 --- a/lua/oil/layout.lua +++ b/lua/oil/layout.lua @@ -93,6 +93,89 @@ M.calculate_height = function(desired_height, opts) ) end +---@class (exact) conform.WinLayout +---@field width integer +---@field height integer +---@field row integer +---@field col integer + +---@return vim.api.keyset.win_config +M.get_fullscreen_win_opts = function() + local config = require("oil.config") + + local total_width = M.get_editor_width() + local total_height = M.get_editor_height() + local width = total_width - 2 * config.float.padding + if config.float.border ~= "none" then + width = width - 2 -- The border consumes 1 col on each side + end + if config.float.max_width > 0 then + width = math.min(width, config.float.max_width) + end + local height = total_height - 2 * config.float.padding + if config.float.max_height > 0 then + height = math.min(height, config.float.max_height) + end + local row = math.floor((total_height - height) / 2) + local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width + + local win_opts = { + relative = "editor", + width = width, + height = height, + row = row, + col = col, + border = config.float.border, + zindex = 45, + } + return config.float.override(win_opts) or win_opts +end + +---@param winid integer +---@param direction "above"|"below"|"left"|"right"|"auto" +---@param gap integer +---@return conform.WinLayout root_dim New dimensions of the original window +---@return conform.WinLayout new_dim New dimensions of the new window +M.split_window = function(winid, direction, gap) + if direction == "auto" then + direction = vim.o.splitright and "right" or "left" + end + + local float_config = vim.api.nvim_win_get_config(winid) + local dim_root = { + width = float_config.width, + height = float_config.height, + col = float_config.col, + row = float_config.row, + } + if vim.fn.has("nvim-0.10") == 0 then + -- read https://github.com/neovim/neovim/issues/24430 for more infos. + dim_root.col = float_config.col[vim.val_idx] + dim_root.row = float_config.row[vim.val_idx] + end + local dim_new = vim.deepcopy(dim_root) + + if direction == "left" or direction == "right" then + dim_new.width = math.floor(float_config.width / 2) - math.ceil(gap / 2) + dim_root.width = dim_new.width + else + dim_new.height = math.floor(float_config.height / 2) - math.ceil(gap / 2) + dim_root.height = dim_new.height + end + + if direction == "left" then + dim_root.col = dim_root.col + dim_root.width + gap + elseif direction == "right" then + dim_new.col = dim_new.col + dim_new.width + gap + elseif direction == "above" then + dim_root.row = dim_root.row + dim_root.height + gap + elseif direction == "below" then + dim_new.row = dim_new.row + dim_new.height + gap + end + + return dim_root, dim_new +end + M.calculate_dims = function(desired_width, desired_height, opts) local width = M.calculate_width(desired_width, opts) local height = M.calculate_height(desired_height, opts) From 4c574cf4a2de736d2662d52ce086d8bdf87c49df Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Wed, 19 Jun 2024 21:41:05 -0400 Subject: [PATCH 047/206] fix: increase loading display delay to avoid flicker (#424) --- lua/oil/loading.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/oil/loading.lua b/lua/oil/loading.lua index 54a3cdd..35a6bba 100644 --- a/lua/oil/loading.lua +++ b/lua/oil/loading.lua @@ -67,7 +67,7 @@ M.set_loading = function(bufnr, is_loading) timers[bufnr] = vim.loop.new_timer() local bar_iter = M.get_bar_iter({ width = width }) timers[bufnr]:start( - 100, -- Delay the loading screen just a bit to avoid flicker + 200, -- Delay the loading screen just a bit to avoid flicker math.floor(1000 / FPS), vim.schedule_wrap(function() if not vim.api.nvim_buf_is_valid(bufnr) or not timers[bufnr] then From f6df58ad370f45dbc18c42ffbaefbcf27df14036 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Thu, 20 Jun 2024 22:45:28 -0400 Subject: [PATCH 048/206] fix: bug in buffer rendering race condition handling --- lua/oil/view.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/oil/view.lua b/lua/oil/view.lua index 3af948c..cfedcdb 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -737,6 +737,7 @@ local function get_used_columns() return cols end +---@type table local pending_renders = {} ---@param bufnr integer @@ -772,7 +773,7 @@ M.render_buffer_async = function(bufnr, opts, callback) vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) util.render_text(bufnr, { "Error: " .. message }) if pending_renders[bufnr] then - for _, cb in ipairs(pending_renders) do + for _, cb in ipairs(pending_renders[bufnr]) do cb(message) end pending_renders[bufnr] = nil From 65c53dbe4f2140236590a7568a5f22a77d16be39 Mon Sep 17 00:00:00 2001 From: DerpDays <34582078+DerpDays@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:07:03 +0100 Subject: [PATCH 049/206] fix: correctly check group permissions in unix (#428) * fix: set modifiable when user in group * feat: add mode caching, fallback to previous, and better checking of permissions * fix: make is_modifiable check group permissions even if the user is owner of the directory * refactor: simplify group ID caching --------- Co-authored-by: Steven Arcangeli --- lua/oil/adapters/files.lua | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua index d502c90..e007423 100644 --- a/lua/oil/adapters/files.lua +++ b/lua/oil/adapters/files.lua @@ -412,6 +412,26 @@ M.list = function(url, column_defs, cb) end, 10000) end +---@type nil|integer[] +local _group_ids +---@return integer[] +local function get_group_ids() + if not _group_ids then + local output = vim.fn.system({ "id", "-G" }) + if vim.v.shell_error == 0 then + _group_ids = vim.tbl_map(tonumber, vim.split(output, "%s+", { trimempty = true })) + else + -- If the id command fails, fall back to just using the process group + _group_ids = { uv.getgid() } + vim.notify( + "[oil] missing the `id` command. Some directories may not be modifiable even if you have group access.", + vim.log.levels.WARN + ) + end + end + return _group_ids +end + ---@param bufnr integer ---@return boolean M.is_modifiable = function(bufnr) @@ -433,14 +453,12 @@ M.is_modifiable = function(bufnr) end local uid = uv.getuid() - local gid = uv.getgid() - local rwx + local rwx = stat.mode if uid == stat.uid then - rwx = bit.rshift(stat.mode, 6) - elseif gid == stat.gid then - rwx = bit.rshift(stat.mode, 3) - else - rwx = stat.mode + rwx = bit.bor(rwx, bit.rshift(stat.mode, 6)) + end + if vim.tbl_contains(get_group_ids(), stat.gid) then + rwx = bit.bor(rwx, bit.rshift(stat.mode, 3)) end return bit.band(rwx, 2) ~= 0 end From c7c7ce5bd47030ee9c60a859f25695647610b8bd Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Mon, 1 Jul 2024 11:34:02 -0700 Subject: [PATCH 050/206] feat: rename experimental_watch_for_changes -> watch_for_changes I've been using this for a while now and haven't seen any issues. We can take "experimental" out of the name. --- README.md | 2 +- doc/oil.txt | 2 +- lua/oil/config.lua | 7 ++++++- lua/oil/types.lua | 2 +- lua/oil/view.lua | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 524657d..52d6cf5 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ require("oil").setup({ -- Set to `false` to disable, or "name" to keep it on the file names constrain_cursor = "editable", -- Set to true to watch the filesystem for changes and reload oil - experimental_watch_for_changes = false, + watch_for_changes = false, -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) -- Additionally, if it is a string that matches "actions.", diff --git a/doc/oil.txt b/doc/oil.txt index a10cd9f..f34aa1a 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -65,7 +65,7 @@ CONFIG *oil-confi -- Set to `false` to disable, or "name" to keep it on the file names constrain_cursor = "editable", -- Set to true to watch the filesystem for changes and reload oil - experimental_watch_for_changes = false, + watch_for_changes = false, -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) -- Additionally, if it is a string that matches "actions.", diff --git a/lua/oil/config.lua b/lua/oil/config.lua index e1dd950..ba52ed1 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -50,7 +50,7 @@ local default_config = { -- Set to `false` to disable, or "name" to keep it on the file names constrain_cursor = "editable", -- Set to true to watch the filesystem for changes and reload oil - experimental_watch_for_changes = false, + watch_for_changes = false, -- Keymaps in oil buffer. Can be any value that `vim.keymap.set` accepts OR a table of keymap -- options with a `callback` (e.g. { callback = function() ... end, desc = "", mode = "n" }) -- Additionally, if it is a string that matches "actions.", @@ -207,6 +207,11 @@ M.setup = function(opts) ) end + -- This option was renamed because it is no longer experimental + if new_conf.experimental_watch_for_changes then + new_conf.watch_for_changes = true + end + for k, v in pairs(new_conf) do M[k] = v end diff --git a/lua/oil/types.lua b/lua/oil/types.lua index d7a3a0a..ef6d26b 100644 --- a/lua/oil/types.lua +++ b/lua/oil/types.lua @@ -9,7 +9,7 @@ ---@field cleanup_delay_ms? integer Oil will automatically delete hidden buffers after this delay. You can set the delay to false to disable cleanup entirely. Note that the cleanup process only starts when none of the oil buffers are currently displayed. ---@field lsp_file_methods? oil.LspFileMethods Configure LSP file operation integration. ---@field constrain_cursor? false|"name"|"editable" Constrain the cursor to the editable parts of the oil buffer. Set to `false` to disable, or "name" to keep it on the file names. ----@field experimental_watch_for_changes? boolean Set to true to watch the filesystem for changes and reload oil. +---@field watch_for_changes? boolean Set to true to watch the filesystem for changes and reload oil. ---@field keymaps? table ---@field use_default_keymaps? boolean Set to false to disable all of the above keymaps ---@field view_options? oil.ViewOptions Configure which files are shown and how they are shown. diff --git a/lua/oil/view.lua b/lua/oil/view.lua index cfedcdb..36cc179 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -447,7 +447,7 @@ M.initialize = function(bufnr) if adapter and adapter.name == "files" - and config.experimental_watch_for_changes + and config.watch_for_changes and not session[bufnr].fs_event then local fs_event = assert(uv.new_fs_event()) From 2077cc3358f327aca16c376cdde6ea0b07f14449 Mon Sep 17 00:00:00 2001 From: Philipp Oeschger <49947694+PhilippOesch@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:41:04 +0200 Subject: [PATCH 051/206] feat: case insensitive sorting (#429) * check for sorting option in netrw * documentation * refactor: remove sort_ prefix --------- Co-authored-by: Steven Arcangeli --- README.md | 2 ++ doc/oil.txt | 2 ++ lua/oil/columns.lua | 12 +++++++++--- lua/oil/config.lua | 2 ++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 52d6cf5..7fdb06a 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,8 @@ require("oil").setup({ -- Sort file names in a more intuitive order for humans. Is less performant, -- so you may want to set to false if you work with large directories. natural_order = true, + -- Sort file and directory names case insensitive + case_insensitive = false, sort = { -- sort order can be "asc" or "desc" -- see :help oil-columns to see which columns are sortable diff --git a/doc/oil.txt b/doc/oil.txt index f34aa1a..ac19879 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -106,6 +106,8 @@ CONFIG *oil-confi -- Sort file names in a more intuitive order for humans. Is less performant, -- so you may want to set to false if you work with large directories. natural_order = true, + -- Sort file and directory names case insensitive + case_insensitive = false, sort = { -- sort order can be "asc" or "desc" -- see :help oil-columns to see which columns are sortable diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua index 3bdd286..453b4e7 100644 --- a/lua/oil/columns.lua +++ b/lua/oil/columns.lua @@ -300,11 +300,17 @@ M.register("name", { end, get_sort_value = function(entry) + local sort_value = entry[FIELD_NAME] + if config.view_options.natural_order then - return entry[FIELD_NAME]:gsub("%d+", pad_number) - else - return entry[FIELD_NAME] + sort_value = sort_value:gsub("%d+", pad_number) end + + if config.view_options.case_insensitive then + sort_value = sort_value:lower() + end + + return sort_value end, }) diff --git a/lua/oil/config.lua b/lua/oil/config.lua index ba52ed1..3fef2e6 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -91,6 +91,8 @@ local default_config = { -- Sort file names in a more intuitive order for humans. Is less performant, -- so you may want to set to false if you work with large directories. natural_order = true, + -- Sort file and directory names case insensitive + case_insensitive = false, sort = { -- sort order can be "asc" or "desc" -- see :help oil-columns to see which columns are sortable From ace46a41a1ea56d544a3775adf30490f2abd9235 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:51:47 -0700 Subject: [PATCH 052/206] chore(master): release 2.11.0 (#427) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 280d010..80f35cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [2.11.0](https://github.com/stevearc/oil.nvim/compare/v2.10.0...v2.11.0) (2024-07-01) + + +### Features + +* case insensitive sorting ([#429](https://github.com/stevearc/oil.nvim/issues/429)) ([2077cc3](https://github.com/stevearc/oil.nvim/commit/2077cc3358f327aca16c376cdde6ea0b07f14449)) +* rename experimental_watch_for_changes -> watch_for_changes ([c7c7ce5](https://github.com/stevearc/oil.nvim/commit/c7c7ce5bd47030ee9c60a859f25695647610b8bd)) +* support preview from floating window ([#403](https://github.com/stevearc/oil.nvim/issues/403)) ([59b3dab](https://github.com/stevearc/oil.nvim/commit/59b3dab6f79e147a0d694ee72c26ae883d323340)) + + +### Bug Fixes + +* bug in buffer rendering race condition handling ([f6df58a](https://github.com/stevearc/oil.nvim/commit/f6df58ad370f45dbc18c42ffbaefbcf27df14036)) +* correctly check group permissions in unix ([#428](https://github.com/stevearc/oil.nvim/issues/428)) ([65c53db](https://github.com/stevearc/oil.nvim/commit/65c53dbe4f2140236590a7568a5f22a77d16be39)) +* increase loading display delay to avoid flicker ([#424](https://github.com/stevearc/oil.nvim/issues/424)) ([4c574cf](https://github.com/stevearc/oil.nvim/commit/4c574cf4a2de736d2662d52ce086d8bdf87c49df)) + ## [2.10.0](https://github.com/stevearc/oil.nvim/compare/v2.9.0...v2.10.0) (2024-06-16) From b0a6cf98982cdcf82b19b0029b734bbbcd24bcc4 Mon Sep 17 00:00:00 2001 From: icefed Date: Wed, 3 Jul 2024 01:03:08 +0800 Subject: [PATCH 053/206] fix: set floating window win_options when buffer changes (#432) * fix: set floating window win_options when buffer changes * fix: set win options even when float border is "none" --------- Co-authored-by: Steven Arcangeli --- lua/oil/init.lua | 58 ++++++++++++++++++++---------------- lua/oil/mutator/progress.lua | 2 +- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 7bcb3bb..e5e21ed 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -286,29 +286,35 @@ M.open_float = function(dir) }) ) - -- Update the window title when we switch buffers - if vim.fn.has("nvim-0.9") == 1 and config.float.border ~= "none" then - local function get_title() - local src_buf = vim.api.nvim_win_get_buf(winid) - local title = vim.api.nvim_buf_get_name(src_buf) - local scheme, path = util.parse_url(title) - if config.adapters[scheme] == "files" then - assert(path) - local fs = require("oil.fs") - title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") - end - return title + ---Recalculate the window title for the current buffer + local function get_title() + local src_buf = vim.api.nvim_win_get_buf(winid) + local title = vim.api.nvim_buf_get_name(src_buf) + local scheme, path = util.parse_url(title) + if config.adapters[scheme] == "files" then + assert(path) + local fs = require("oil.fs") + title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") end - table.insert( - autocmds, - vim.api.nvim_create_autocmd("BufWinEnter", { - desc = "Update oil floating window title when buffer changes", - pattern = "*", - callback = function(params) - local winbuf = params.buf - if not vim.api.nvim_win_is_valid(winid) or vim.api.nvim_win_get_buf(winid) ~= winbuf then - return - end + return title + end + + table.insert( + autocmds, + vim.api.nvim_create_autocmd("BufWinEnter", { + desc = "Reset local oil window options when buffer changes", + pattern = "*", + callback = function(params) + local winbuf = params.buf + if not vim.api.nvim_win_is_valid(winid) or vim.api.nvim_win_get_buf(winid) ~= winbuf then + return + end + for k, v in pairs(config.float.win_options) do + vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) + end + + -- Update the floating window title + if vim.fn.has("nvim-0.9") == 1 and config.float.border ~= "none" then local cur_win_opts = vim.api.nvim_win_get_config(winid) vim.api.nvim_win_set_config(winid, { relative = "editor", @@ -318,10 +324,10 @@ M.open_float = function(dir) height = cur_win_opts.height, title = get_title(), }) - end, - }) - ) - end + end + end, + }) + ) vim.cmd.edit({ args = { util.escape_filename(parent_url) }, mods = { keepalt = true } }) -- :edit will set buflisted = true, but we may not want that diff --git a/lua/oil/mutator/progress.lua b/lua/oil/mutator/progress.lua index 58e8587..057f0a0 100644 --- a/lua/oil/mutator/progress.lua +++ b/lua/oil/mutator/progress.lua @@ -69,7 +69,7 @@ function Progress:show(opts) border = config.progress.border, }) vim.bo[self.bufnr].filetype = "oil_progress" - for k, v in pairs(config.preview.win_options) do + for k, v in pairs(config.progress.win_options) do vim.api.nvim_set_option_value(k, v, { scope = "local", win = self.winid }) end table.insert( From b15e4c1e647b9ddbb75a31caeb720b3b3ce4db54 Mon Sep 17 00:00:00 2001 From: icefed Date: Wed, 3 Jul 2024 01:31:26 +0800 Subject: [PATCH 054/206] feat: disable cursor in preview window (#433) --- lua/oil/mutator/preview.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lua/oil/mutator/preview.lua b/lua/oil/mutator/preview.lua index 64cd520..31588af 100644 --- a/lua/oil/mutator/preview.lua +++ b/lua/oil/mutator/preview.lua @@ -114,6 +114,11 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) render_lines(winid, bufnr, lines) + -- Disable cursor + -- We are in the preview window now, so no need to use autocmd with WinEnter + vim.api.nvim_set_hl(0, "OilPreviewCursor", { nocombine = true, blend = 100 }) + vim.opt.guicursor:append("a:OilPreviewCursor/OilPreviewCursor") + -- Attach autocmds and keymaps local cancel local confirm @@ -127,6 +132,9 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) end autocmds = {} vim.api.nvim_win_close(winid, true) + -- restore cursor + vim.api.nvim_set_hl(0, "OilPreviewCursor", {}) + vim.opt.guicursor:remove("a:OilPreviewCursor/OilPreviewCursor") cb(value) end end From b5a1abfde00eead6814cae3321e4c90ff98cfff1 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Wed, 3 Jul 2024 18:14:52 -0700 Subject: [PATCH 055/206] fix: cursor sometimes disappears after making changes (#438) --- lua/oil/mutator/preview.lua | 9 ++------- lua/oil/util.lua | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lua/oil/mutator/preview.lua b/lua/oil/mutator/preview.lua index 31588af..3f8d87d 100644 --- a/lua/oil/mutator/preview.lua +++ b/lua/oil/mutator/preview.lua @@ -114,10 +114,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) render_lines(winid, bufnr, lines) - -- Disable cursor - -- We are in the preview window now, so no need to use autocmd with WinEnter - vim.api.nvim_set_hl(0, "OilPreviewCursor", { nocombine = true, blend = 100 }) - vim.opt.guicursor:append("a:OilPreviewCursor/OilPreviewCursor") + local restore_cursor = util.hide_cursor() -- Attach autocmds and keymaps local cancel @@ -132,9 +129,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) end autocmds = {} vim.api.nvim_win_close(winid, true) - -- restore cursor - vim.api.nvim_set_hl(0, "OilPreviewCursor", {}) - vim.opt.guicursor:remove("a:OilPreviewCursor/OilPreviewCursor") + restore_cursor() cb(value) end end diff --git a/lua/oil/util.lua b/lua/oil/util.lua index a685b14..a1a95ab 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -665,6 +665,20 @@ M.get_preview_win = function() end end +---@return fun() restore Function that restores the cursor +M.hide_cursor = function() + vim.api.nvim_set_hl(0, "OilPreviewCursor", { nocombine = true, blend = 100 }) + local original_guicursor = vim.go.guicursor + vim.go.guicursor = "a:OilPreviewCursor/OilPreviewCursor" + + return function() + -- HACK: see https://github.com/neovim/neovim/issues/21018 + vim.go.guicursor = "a:" + vim.cmd.redrawstatus() + vim.go.guicursor = original_guicursor + end +end + ---@param bufnr integer ---@param preferred_win nil|integer ---@return nil|integer From a543ea598eaef3363fe253e0e11837c1404eb04d Mon Sep 17 00:00:00 2001 From: Micah Halter Date: Fri, 5 Jul 2024 18:13:10 -0400 Subject: [PATCH 056/206] feat: add support for `mini.icons` (#439) --- README.md | 7 +++++-- lua/oil/columns.lua | 17 +++++------------ lua/oil/util.lua | 28 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7fdb06a..4e6a4b4 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94 ## Requirements - Neovim 0.8+ -- (optional) [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons +- Icon provider plugin (optional) + - [mini.icons](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-icons.md) for file and folder icons + - [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons ## Installation @@ -34,7 +36,8 @@ oil.nvim supports all the usual plugin managers 'stevearc/oil.nvim', opts = {}, -- Optional dependencies - dependencies = { "nvim-tree/nvim-web-devicons" }, + dependencies = { "echasnovski/mini.icons" }, + -- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if prefer nvim-web-devicons } ``` diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua index 453b4e7..97c6fcb 100644 --- a/lua/oil/columns.lua +++ b/lua/oil/columns.lua @@ -1,7 +1,6 @@ local config = require("oil.config") local constants = require("oil.constants") local util = require("oil.util") -local has_devicons, devicons = pcall(require, "nvim-web-devicons") local M = {} local FIELD_NAME = constants.FIELD_NAME @@ -202,7 +201,8 @@ M.perform_change_action = function(adapter, action, callback) column.perform_action(action, callback) end -if has_devicons then +local icon_provider = util.get_icon_provider() +if icon_provider then M.register("icon", { render = function(entry, conf) local field_type = entry[FIELD_TYPE] @@ -216,17 +216,10 @@ if has_devicons then field_type = meta.link_stat.type end end - local icon, hl - if field_type == "directory" then - icon = conf and conf.directory or "" - hl = "OilDirIcon" - else - if meta and meta.display_name then - name = meta.display_name - end - icon, hl = devicons.get_icon(name) - icon = icon or (conf and conf.default_file or "") + if meta and meta.display_name then + name = meta.display_name end + local icon, hl = icon_provider(field_type, name, conf) if not conf or conf.add_padding ~= false then icon = icon .. " " end diff --git a/lua/oil/util.lua b/lua/oil/util.lua index a1a95ab..c84c1e9 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -8,6 +8,8 @@ local FIELD_NAME = constants.FIELD_NAME local FIELD_TYPE = constants.FIELD_TYPE local FIELD_META = constants.FIELD_META +---@alias oil.IconProvider fun(type: string, name: string, conf: table?): (icon: string, hl: string) + ---@param url string ---@return nil|string ---@return nil|string @@ -858,4 +860,30 @@ M.get_edit_path = function(bufnr, entry, callback) end end +--- Check for an icon provider and return a common icon provider API +---@return (oil.IconProvider)? +M.get_icon_provider = function() + -- prefer mini.icons + local has_mini_icons, mini_icons = pcall(require, "mini.icons") + if has_mini_icons then + return function(type, name) + return mini_icons.get(type == "directory" and "directory" or "file", name) + end + end + + -- fallback to `nvim-web-devicons` + local has_devicons, devicons = pcall(require, "nvim-web-devicons") + if has_devicons then + return function(type, name, conf) + if type == "directory" then + return conf and conf.directory or "", "OilDirIcon" + else + local icon, hl = devicons.get_icon(name) + icon = icon or (conf and conf.default_file or "") + return icon, hl + end + end + end +end + return M From d5e56574f896120b78cdf56dc1132e76057f8877 Mon Sep 17 00:00:00 2001 From: Micah Halter Date: Sat, 6 Jul 2024 19:20:44 -0400 Subject: [PATCH 057/206] fix: correctly check if `mini.icons` is actually setup (#441) This leaves the `pcall` just so (1) we load the plugin if it is lazy loaded by the user and (2) we get LSP completion/validation with that type as well. --- lua/oil/util.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/oil/util.lua b/lua/oil/util.lua index c84c1e9..aa717ad 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -864,8 +864,9 @@ end ---@return (oil.IconProvider)? M.get_icon_provider = function() -- prefer mini.icons - local has_mini_icons, mini_icons = pcall(require, "mini.icons") - if has_mini_icons then + local _, mini_icons = pcall(require, "mini.icons") + ---@diagnostic disable-next-line: undefined-field + if _G.MiniIcons then -- `_G.MiniIcons` is a better check to see if the module is setup return function(type, name) return mini_icons.get(type == "directory" and "directory" or "file", name) end From cc2332599f8944076fba29ff7960729b3fcdd71b Mon Sep 17 00:00:00 2001 From: sleeptightAnsiC <91839286+sleeptightAnsiC@users.noreply.github.com> Date: Sun, 7 Jul 2024 01:24:33 +0200 Subject: [PATCH 058/206] feat: allow bufnr optional parameter for get_current_dir function (#440) This allows for using get_current_dir in cases where currently hovered buffer is not the desired Oil buffer (e.g. displaying directories for multiple different Oil buffers) --- doc/api.md | 8 ++++++-- doc/oil.txt | 4 +++- lua/oil/init.lua | 6 ++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/doc/api.md b/doc/api.md index 77a07da..411c252 100644 --- a/doc/api.md +++ b/doc/api.md @@ -83,11 +83,15 @@ Change how oil determines if the file is hidden Toggle hidden files and directories -## get_current_dir() +## get_current_dir(bufnr) -`get_current_dir(): nil|string` \ +`get_current_dir(bufnr): nil|string` \ Get the current directory +| Param | Type | Desc | +| ----- | -------------- | --------------------------------------------- | +| bufnr | `nil\|integer` | When nil, get directory of the current buffer | + ## open_float(dir) diff --git a/doc/oil.txt b/doc/oil.txt index ac19879..1329871 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -277,9 +277,11 @@ toggle_hidden() *oil.toggle_hidde Toggle hidden files and directories -get_current_dir(): nil|string *oil.get_current_dir* +get_current_dir({bufnr}): nil|string *oil.get_current_dir* Get the current directory + Parameters: + {bufnr} `integer` When nil, get directory of the current buffer open_float({dir}) *oil.open_float* Open oil browser in a floating window diff --git a/lua/oil/init.lua b/lua/oil/init.lua index e5e21ed..73e3fe8 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -140,12 +140,14 @@ M.toggle_hidden = function() end ---Get the current directory +---@param bufnr? integer ---@return nil|string -M.get_current_dir = function() +M.get_current_dir = function(bufnr) local config = require("oil.config") local fs = require("oil.fs") local util = require("oil.util") - local scheme, path = util.parse_url(vim.api.nvim_buf_get_name(0)) + local buf_name = vim.api.nvim_buf_get_name(bufnr or 0) + local scheme, path = util.parse_url(buf_name) if config.adapters[scheme] == "files" then assert(path) return fs.posix_to_os_path(path) From 10fbfdd37b6904c0776c5db1a27ab47eecba335e Mon Sep 17 00:00:00 2001 From: Github Actions Date: Sat, 6 Jul 2024 23:24:46 +0000 Subject: [PATCH 059/206] [docgen] Update docs skip-checks: true --- README.md | 2 +- doc/api.md | 9 ++++----- doc/oil.txt | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4e6a4b4..1d5fcff 100644 --- a/README.md +++ b/README.md @@ -345,7 +345,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i - [set_sort(sort)](doc/api.md#set_sortsort) - [set_is_hidden_file(is_hidden_file)](doc/api.md#set_is_hidden_fileis_hidden_file) - [toggle_hidden()](doc/api.md#toggle_hidden) -- [get_current_dir()](doc/api.md#get_current_dir) +- [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr) - [open_float(dir)](doc/api.md#open_floatdir) - [toggle_float(dir)](doc/api.md#toggle_floatdir) - [open(dir)](doc/api.md#opendir) diff --git a/doc/api.md b/doc/api.md index 411c252..0018d5c 100644 --- a/doc/api.md +++ b/doc/api.md @@ -9,7 +9,7 @@ - [set_sort(sort)](#set_sortsort) - [set_is_hidden_file(is_hidden_file)](#set_is_hidden_fileis_hidden_file) - [toggle_hidden()](#toggle_hidden) -- [get_current_dir()](#get_current_dir) +- [get_current_dir(bufnr)](#get_current_dirbufnr) - [open_float(dir)](#open_floatdir) - [toggle_float(dir)](#toggle_floatdir) - [open(dir)](#opendir) @@ -88,10 +88,9 @@ Toggle hidden files and directories `get_current_dir(bufnr): nil|string` \ Get the current directory -| Param | Type | Desc | -| ----- | -------------- | --------------------------------------------- | -| bufnr | `nil\|integer` | When nil, get directory of the current buffer | - +| Param | Type | Desc | +| ----- | -------------- | ---- | +| bufnr | `nil\|integer` | | ## open_float(dir) diff --git a/doc/oil.txt b/doc/oil.txt index 1329871..9582cab 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -281,7 +281,7 @@ get_current_dir({bufnr}): nil|string *oil.get_current_di Get the current directory Parameters: - {bufnr} `integer` When nil, get directory of the current buffer + {bufnr} `nil|integer` open_float({dir}) *oil.open_float* Open oil browser in a floating window From 9e5eb2fcd1dfee2ff30c89273ffff179e42034b9 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Mon, 15 Jul 2024 14:01:56 -0700 Subject: [PATCH 060/206] doc: make lazy.nvim snippet more copy/paste-able (#445) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d5fcff..1c1edd9 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ oil.nvim supports all the usual plugin managers 'stevearc/oil.nvim', opts = {}, -- Optional dependencies - dependencies = { "echasnovski/mini.icons" }, + dependencies = { { "echasnovski/mini.icons", opts = {} } }, -- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if prefer nvim-web-devicons } ``` From a6cea1a5b9bc9351769fe09a547c62fe4b669abd Mon Sep 17 00:00:00 2001 From: Anna Arad <4895022+annagrram@users.noreply.github.com> Date: Mon, 22 Jul 2024 02:07:10 +0300 Subject: [PATCH 061/206] fix: Handle users and groups with spaces over SSH (#448) --- lua/oil/adapters/ssh/connection.lua | 4 ++-- lua/oil/adapters/ssh/sshfs.lua | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lua/oil/adapters/ssh/connection.lua b/lua/oil/adapters/ssh/connection.lua index a6bca02..146b140 100644 --- a/lua/oil/adapters/ssh/connection.lua +++ b/lua/oil/adapters/ssh/connection.lua @@ -156,7 +156,7 @@ function SSHConnection.new(url) else self.jid = jid end - self:run("whoami", function(err, lines) + self:run("id -u", function(err, lines) if err then vim.notify(string.format("Error fetching ssh connection user: %s", err), vim.log.levels.WARN) else @@ -164,7 +164,7 @@ function SSHConnection.new(url) self.meta.user = vim.trim(table.concat(lines, "")) end end) - self:run("groups", function(err, lines) + self:run("id -G", function(err, lines) if err then vim.notify( string.format("Error fetching ssh connection user groups: %s", err), diff --git a/lua/oil/adapters/ssh/sshfs.lua b/lua/oil/adapters/ssh/sshfs.lua index 7cc3c65..1ae2440 100644 --- a/lua/oil/adapters/ssh/sshfs.lua +++ b/lua/oil/adapters/ssh/sshfs.lua @@ -27,7 +27,7 @@ local typechar_map = { ---@return table Metadata for entry local function parse_ls_line(line) local typechar, perms, refcount, user, group, rem = - line:match("^(.)(%S+)%s+(%d+)%s+(%S+)%s+(%S+)%s+(.*)$") + line:match("^(.)(%S+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(.*)$") if not typechar then error(string.format("Could not parse '%s'", line)) end @@ -112,7 +112,7 @@ function SSHFS:realpath(path, callback) abspath = abspath:sub(1, #abspath - 1) end self.conn:run( - string.format("ls -ald --color=never %s", shellescape(abspath)), + string.format("ls -land --color=never %s", shellescape(abspath)), function(ls_err, ls_lines) local type if ls_err then @@ -142,7 +142,7 @@ function SSHFS:list_dir(url, path, callback) if path ~= "" then path_postfix = string.format(" %s", shellescape(path)) end - self.conn:run("LANG=C ls -al --color=never" .. path_postfix, function(err, lines) + self.conn:run("LANG=C ls -lan --color=never" .. path_postfix, function(err, lines) if err then if err:match("No such file or directory%s*$") then -- If the directory doesn't exist, treat the list as a success. We will be able to traverse @@ -176,7 +176,7 @@ function SSHFS:list_dir(url, path, callback) -- If there were any soft links, then we need to run another ls command with -L so that we can -- resolve the type of the link target self.conn:run( - "ls -aLl --color=never" .. path_postfix .. " 2> /dev/null", + "ls -naLl --color=never" .. path_postfix .. " 2> /dev/null", function(link_err, link_lines) -- Ignore exit code 1. That just means one of the links could not be resolved. if link_err and not link_err:match("^1:") then From 71c972fbd218723a3c15afcb70421f67340f5a6d Mon Sep 17 00:00:00 2001 From: Anna Arad <4895022+annagrram@users.noreply.github.com> Date: Tue, 23 Jul 2024 07:17:38 +0300 Subject: [PATCH 062/206] fix: Force standard C locale when getting `ls` input for parsing in SSH (#455) --- lua/oil/adapters/ssh/sshfs.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/oil/adapters/ssh/sshfs.lua b/lua/oil/adapters/ssh/sshfs.lua index 1ae2440..3b9faf3 100644 --- a/lua/oil/adapters/ssh/sshfs.lua +++ b/lua/oil/adapters/ssh/sshfs.lua @@ -112,7 +112,7 @@ function SSHFS:realpath(path, callback) abspath = abspath:sub(1, #abspath - 1) end self.conn:run( - string.format("ls -land --color=never %s", shellescape(abspath)), + string.format("LC_ALL=C ls -land --color=never %s", shellescape(abspath)), function(ls_err, ls_lines) local type if ls_err then @@ -142,7 +142,7 @@ function SSHFS:list_dir(url, path, callback) if path ~= "" then path_postfix = string.format(" %s", shellescape(path)) end - self.conn:run("LANG=C ls -lan --color=never" .. path_postfix, function(err, lines) + self.conn:run("LC_ALL=C ls -lan --color=never" .. path_postfix, function(err, lines) if err then if err:match("No such file or directory%s*$") then -- If the directory doesn't exist, treat the list as a success. We will be able to traverse @@ -176,7 +176,7 @@ function SSHFS:list_dir(url, path, callback) -- If there were any soft links, then we need to run another ls command with -L so that we can -- resolve the type of the link target self.conn:run( - "ls -naLl --color=never" .. path_postfix .. " 2> /dev/null", + "LC_ALL=C ls -naLl --color=never" .. path_postfix .. " 2> /dev/null", function(link_err, link_lines) -- Ignore exit code 1. That just means one of the links could not be resolved. if link_err and not link_err:match("^1:") then From fcca212c2e966fc3dec1d4baf888e670631d25d1 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Mon, 29 Jul 2024 17:12:20 -0700 Subject: [PATCH 063/206] fix: handle rare case where file watcher outlives buffer --- lua/oil/view.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lua/oil/view.lua b/lua/oil/view.lua index 36cc179..aa996fd 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -457,6 +457,14 @@ M.initialize = function(bufnr) assert(dir), {}, vim.schedule_wrap(function(err, filename, events) + if not vim.api.nvim_buf_is_valid(bufnr) then + local sess = session[bufnr] + if sess then + sess.fs_event = nil + end + fs_event:stop() + return + end local mutator = require("oil.mutator") if err or vim.bo[bufnr].modified or vim.b[bufnr].oil_dirty or mutator.is_mutating() then return From b39a78959f3f69e9c1bf43c2634bbddf0af51c3e Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 17 Aug 2024 06:33:59 +0200 Subject: [PATCH 064/206] fix: add compatibility for Lua 5.1 (#456) Some architectures don't support LuaJIT. Remove the goto's from the code to be compatible with Neovim built without LuaJIT. Signed-off-by: Julian Ruess --- lua/oil/mutator/parser.lua | 196 +++++++++++++++++++------------------ lua/oil/view.lua | 20 ++-- 2 files changed, 109 insertions(+), 107 deletions(-) diff --git a/lua/oil/mutator/parser.lua b/lua/oil/mutator/parser.lua index 8304a9c..d2d3590 100644 --- a/lua/oil/mutator/parser.lua +++ b/lua/oil/mutator/parser.lua @@ -192,115 +192,119 @@ M.parse = function(bufnr) seen_names[name] = true end end + for i, line in ipairs(lines) do - if line:match("^/%d+") then - -- Parse the line for an existing entry - local result, err = M.parse_line(adapter, line, column_defs) - if not result or err then - table.insert(errors, { - message = err, - lnum = i - 1, - end_lnum = i, - col = 0, - }) - goto continue - elseif result.data.id == 0 then - -- Ignore entries with ID 0 (typically the "../" entry) - goto continue - end - local parsed_entry = result.data - local entry = result.entry - - local err_message - if not parsed_entry.name then - err_message = "No filename found" - elseif not entry then - err_message = "Could not find existing entry (was the ID changed?)" - elseif parsed_entry.name:match("/") or parsed_entry.name:match(fs.sep) then - err_message = "Filename cannot contain path separator" - end - if err_message then - table.insert(errors, { - message = err_message, - lnum = i - 1, - end_lnum = i, - col = 0, - }) - goto continue - end - assert(entry) - - check_dupe(parsed_entry.name, i) - local meta = entry[FIELD_META] - if original_entries[parsed_entry.name] == parsed_entry.id then - if entry[FIELD_TYPE] == "link" and not compare_link_target(meta, parsed_entry) then - table.insert(diffs, { - type = "new", - name = parsed_entry.name, - entry_type = "link", - link = parsed_entry.link_target, + -- hack to be compatible with Lua 5.1 + -- use return instead of goto + (function() + if line:match("^/%d+") then + -- Parse the line for an existing entry + local result, err = M.parse_line(adapter, line, column_defs) + if not result or err then + table.insert(errors, { + message = err, + lnum = i - 1, + end_lnum = i, + col = 0, }) - elseif entry[FIELD_TYPE] ~= parsed_entry._type then + return + elseif result.data.id == 0 then + -- Ignore entries with ID 0 (typically the "../" entry) + return + end + local parsed_entry = result.data + local entry = result.entry + + local err_message + if not parsed_entry.name then + err_message = "No filename found" + elseif not entry then + err_message = "Could not find existing entry (was the ID changed?)" + elseif parsed_entry.name:match("/") or parsed_entry.name:match(fs.sep) then + err_message = "Filename cannot contain path separator" + end + if err_message then + table.insert(errors, { + message = err_message, + lnum = i - 1, + end_lnum = i, + col = 0, + }) + return + end + assert(entry) + + check_dupe(parsed_entry.name, i) + local meta = entry[FIELD_META] + if original_entries[parsed_entry.name] == parsed_entry.id then + if entry[FIELD_TYPE] == "link" and not compare_link_target(meta, parsed_entry) then + table.insert(diffs, { + type = "new", + name = parsed_entry.name, + entry_type = "link", + link = parsed_entry.link_target, + }) + elseif entry[FIELD_TYPE] ~= parsed_entry._type then + table.insert(diffs, { + type = "new", + name = parsed_entry.name, + entry_type = parsed_entry._type, + }) + else + original_entries[parsed_entry.name] = nil + end + else table.insert(diffs, { type = "new", name = parsed_entry.name, entry_type = parsed_entry._type, + id = parsed_entry.id, + link = parsed_entry.link_target, }) - else - original_entries[parsed_entry.name] = nil + end + + for _, col_def in ipairs(column_defs) do + local col_name = util.split_config(col_def) + if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then + table.insert(diffs, { + type = "change", + name = parsed_entry.name, + entry_type = entry[FIELD_TYPE], + column = col_name, + value = parsed_entry[col_name], + }) + end end else - table.insert(diffs, { - type = "new", - name = parsed_entry.name, - entry_type = parsed_entry._type, - id = parsed_entry.id, - link = parsed_entry.link_target, - }) - end - - for _, col_def in ipairs(column_defs) do - local col_name = util.split_config(col_def) - if columns.compare(adapter, col_name, entry, parsed_entry[col_name]) then + -- Parse a new entry + local name, isdir = parsedir(vim.trim(line)) + if vim.startswith(name, "/") then + table.insert(errors, { + message = "Paths cannot start with '/'", + lnum = i - 1, + end_lnum = i, + col = 0, + }) + return + end + if name ~= "" then + local link_pieces = vim.split(name, " -> ", { plain = true }) + local entry_type = isdir and "directory" or "file" + local link + if #link_pieces == 2 then + entry_type = "link" + name, link = unpack(link_pieces) + end + check_dupe(name, i) table.insert(diffs, { - type = "change", - name = parsed_entry.name, - entry_type = entry[FIELD_TYPE], - column = col_name, - value = parsed_entry[col_name], + type = "new", + name = name, + entry_type = entry_type, + link = link, }) end end - else - -- Parse a new entry - local name, isdir = parsedir(vim.trim(line)) - if vim.startswith(name, "/") then - table.insert(errors, { - message = "Paths cannot start with '/'", - lnum = i - 1, - end_lnum = i, - col = 0, - }) - goto continue - end - if name ~= "" then - local link_pieces = vim.split(name, " -> ", { plain = true }) - local entry_type = isdir and "directory" or "file" - local link - if #link_pieces == 2 then - entry_type = "link" - name, link = unpack(link_pieces) - end - check_dupe(name, i) - table.insert(diffs, { - type = "new", - name = name, - entry_type = entry_type, - link = link, - }) - end - end - ::continue:: + end)() end for name, child_id in pairs(original_entries) do diff --git a/lua/oil/view.lua b/lua/oil/view.lua index aa996fd..9b0bf39 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -626,19 +626,17 @@ local function render_buffer(bufnr, opts) end for _, entry in ipairs(entry_list) do - if not M.should_display(entry[FIELD_NAME], bufnr) then - goto continue - end - local cols = M.format_entry_cols(entry, column_defs, col_width, adapter) - table.insert(line_table, cols) + if M.should_display(entry[FIELD_NAME], bufnr) then + local cols = M.format_entry_cols(entry, column_defs, col_width, adapter) + table.insert(line_table, cols) - local name = entry[FIELD_NAME] - if seek_after_render == name then - seek_after_render_found = true - jump_idx = #line_table - M.set_last_cursor(bufname, nil) + local name = entry[FIELD_NAME] + if seek_after_render == name then + seek_after_render_found = true + jump_idx = #line_table + M.set_last_cursor(bufname, nil) + end end - ::continue:: end local lines, highlights = util.render_table(line_table, col_width) From a632c898fbe0e363ef89b9577f1a7714ab67d682 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 21:37:00 -0700 Subject: [PATCH 065/206] chore(master): release 2.12.0 (#434) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f35cb..3bd280a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [2.12.0](https://github.com/stevearc/oil.nvim/compare/v2.11.0...v2.12.0) (2024-08-17) + + +### Features + +* add support for `mini.icons` ([#439](https://github.com/stevearc/oil.nvim/issues/439)) ([a543ea5](https://github.com/stevearc/oil.nvim/commit/a543ea598eaef3363fe253e0e11837c1404eb04d)) +* allow bufnr optional parameter for get_current_dir function ([#440](https://github.com/stevearc/oil.nvim/issues/440)) ([cc23325](https://github.com/stevearc/oil.nvim/commit/cc2332599f8944076fba29ff7960729b3fcdd71b)) +* disable cursor in preview window ([#433](https://github.com/stevearc/oil.nvim/issues/433)) ([b15e4c1](https://github.com/stevearc/oil.nvim/commit/b15e4c1e647b9ddbb75a31caeb720b3b3ce4db54)) + + +### Bug Fixes + +* add compatibility for Lua 5.1 ([#456](https://github.com/stevearc/oil.nvim/issues/456)) ([b39a789](https://github.com/stevearc/oil.nvim/commit/b39a78959f3f69e9c1bf43c2634bbddf0af51c3e)) +* correctly check if `mini.icons` is actually setup ([#441](https://github.com/stevearc/oil.nvim/issues/441)) ([d5e5657](https://github.com/stevearc/oil.nvim/commit/d5e56574f896120b78cdf56dc1132e76057f8877)) +* cursor sometimes disappears after making changes ([#438](https://github.com/stevearc/oil.nvim/issues/438)) ([b5a1abf](https://github.com/stevearc/oil.nvim/commit/b5a1abfde00eead6814cae3321e4c90ff98cfff1)) +* Force standard C locale when getting `ls` input for parsing in SSH ([#455](https://github.com/stevearc/oil.nvim/issues/455)) ([71c972f](https://github.com/stevearc/oil.nvim/commit/71c972fbd218723a3c15afcb70421f67340f5a6d)) +* handle rare case where file watcher outlives buffer ([fcca212](https://github.com/stevearc/oil.nvim/commit/fcca212c2e966fc3dec1d4baf888e670631d25d1)) +* Handle users and groups with spaces over SSH ([#448](https://github.com/stevearc/oil.nvim/issues/448)) ([a6cea1a](https://github.com/stevearc/oil.nvim/commit/a6cea1a5b9bc9351769fe09a547c62fe4b669abd)) +* set floating window win_options when buffer changes ([#432](https://github.com/stevearc/oil.nvim/issues/432)) ([b0a6cf9](https://github.com/stevearc/oil.nvim/commit/b0a6cf98982cdcf82b19b0029b734bbbcd24bcc4)) + ## [2.11.0](https://github.com/stevearc/oil.nvim/compare/v2.10.0...v2.11.0) (2024-07-01) From 349bca8c3eae4ab78629ed63ee55cc3458a367c0 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 25 Aug 2024 20:46:46 -0700 Subject: [PATCH 066/206] fix: process deletes in dir before moving dir --- lua/oil/mutator/init.lua | 7 +++---- tests/mutator_spec.lua | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index b4f5278..ab24a91 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -252,11 +252,10 @@ M.enforce_action_order = function(actions) -- Process children before moving -- e.g. NEW /a/b BEFORE MOVE /a -> /b dest_trie:accum_children_of(action.src_url, ret) - -- Copy children before moving parent dir + -- Process children before moving parent dir -- e.g. COPY /a/b -> /b BEFORE MOVE /a -> /d - src_trie:accum_children_of(action.src_url, ret, function(a) - return a.type == "copy" - end) + -- e.g. CHANGE /a/b BEFORE MOVE /a -> /d + src_trie:accum_children_of(action.src_url, ret) -- Process remove path before moving to new path -- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a src_trie:accum_actions_at(action.dest_url, ret, function(a) diff --git a/tests/mutator_spec.lua b/tests/mutator_spec.lua index 13349e2..17548d3 100644 --- a/tests/mutator_spec.lua +++ b/tests/mutator_spec.lua @@ -216,6 +216,26 @@ a.describe("mutator", function() assert.are.same({ move1, move2 }, ordered_actions) end) + it("Handles a delete inside a moved folder", function() + -- delete in directory and move directory + -- DELETE /a/b.txt + -- MOVE /a/ -> /b/ + local del = { + type = "delete", + url = "oil-test:///a/b.txt", + entry_type = "file", + } + local move = { + type = "move", + src_url = "oil-test:///a", + dest_url = "oil-test:///b", + entry_type = "directory", + } + local actions = { move, del } + local ordered_actions = mutator.enforce_action_order(actions) + assert.are.same({ del, move }, ordered_actions) + end) + it("Detects move directory loops", function() local move = { type = "move", From 70337eb77f53cbff0b7f54f403d5b2b0a9430935 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 25 Aug 2024 20:51:27 -0700 Subject: [PATCH 067/206] fix: gracefully handle trashing file that does not exist --- lua/oil/adapters/trash/freedesktop.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua index 24a6e68..c6221ab 100644 --- a/lua/oil/adapters/trash/freedesktop.lua +++ b/lua/oil/adapters/trash/freedesktop.lua @@ -75,8 +75,13 @@ end ---@param path string ---@return string local function get_write_trash_dir(path) - local dev = uv.fs_lstat(path).dev + local lstat = uv.fs_lstat(path) local home_trash = get_home_trash_dir() + if not lstat then + -- If the source file doesn't exist default to home trash dir + return home_trash + end + local dev = lstat.dev if uv.fs_lstat(home_trash).dev == dev then return home_trash end From 4f3c6780ff272d8facab69a658cf8693582b7a54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 25 Aug 2024 21:03:15 -0700 Subject: [PATCH 068/206] chore(master): release 2.12.1 (#468) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd280a..2c68e2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.12.1](https://github.com/stevearc/oil.nvim/compare/v2.12.0...v2.12.1) (2024-08-26) + + +### Bug Fixes + +* gracefully handle trashing file that does not exist ([70337eb](https://github.com/stevearc/oil.nvim/commit/70337eb77f53cbff0b7f54f403d5b2b0a9430935)) +* process deletes in dir before moving dir ([349bca8](https://github.com/stevearc/oil.nvim/commit/349bca8c3eae4ab78629ed63ee55cc3458a367c0)) + ## [2.12.0](https://github.com/stevearc/oil.nvim/compare/v2.11.0...v2.12.0) (2024-08-17) From 0fcd1263a2e8b6200e2b9fd4ab83d40ed8899c54 Mon Sep 17 00:00:00 2001 From: Micah Halter Date: Wed, 28 Aug 2024 20:13:57 -0400 Subject: [PATCH 069/206] perf(view): avoid running `is_hidden_file` when `show_hidden` is set (#471) --- lua/oil/view.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/oil/view.lua b/lua/oil/view.lua index 9b0bf39..6b085da 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -22,7 +22,7 @@ local last_cursor_entry = {} ---@return boolean M.should_display = function(name, bufnr) return not config.view_options.is_always_hidden(name, bufnr) - and (not config.view_options.is_hidden_file(name, bufnr) or config.view_options.show_hidden) + and (config.view_options.show_hidden or not config.view_options.is_hidden_file(name, bufnr)) end ---@param bufname string From 30e0438ff08f197d7ce4a417445ab97ee72efe2d Mon Sep 17 00:00:00 2001 From: csponge Date: Wed, 28 Aug 2024 20:54:03 -0400 Subject: [PATCH 070/206] fix: ensure win_options are being set on correct window (#469) * Added check for filetype before setting win_options in initialize * refactor: use nvim_buf_call to set window options --------- Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com> --- lua/oil/view.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/oil/view.lua b/lua/oil/view.lua index 6b085da..658bbad 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -333,7 +333,8 @@ M.initialize = function(bufnr) for k, v in pairs(config.buf_options) do vim.bo[bufnr][k] = v end - M.set_win_options() + vim.api.nvim_buf_call(bufnr, M.set_win_options) + vim.api.nvim_create_autocmd("BufHidden", { desc = "Delete oil buffers when no longer in use", group = "Oil", From 85637c1e6383bc8f00e43ac8a172de8cb760a96d Mon Sep 17 00:00:00 2001 From: Micah Halter Date: Thu, 29 Aug 2024 12:26:19 -0400 Subject: [PATCH 071/206] doc(recipes): improve git integrated hidden files recipe (#470) --- doc/recipes.md | 83 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/doc/recipes.md b/doc/recipes.md index 3a78a1f..525ddfc 100644 --- a/doc/recipes.md +++ b/doc/recipes.md @@ -30,47 +30,72 @@ require("oil").setup({ }) ``` -## Hide gitignored files +## Hide gitignored files and show git tracked hidden files ```lua -local git_ignored = setmetatable({}, { - __index = function(self, key) - local proc = vim.system( - { "git", "ls-files", "--ignored", "--exclude-standard", "--others", "--directory" }, - { +-- helper function to parse output +local function parse_output(proc) + local result = proc:wait() + local ret = {} + if result.code == 0 then + for line in vim.gsplit(result.stdout, "\n", { plain = true, trimempty = true }) do + -- Remove trailing slash + line = line:gsub("/$", "") + ret[line] = true + end + end + return ret +end + +-- build git status cache +local function new_git_status() + return setmetatable({}, { + __index = function(self, key) + local ignore_proc = vim.system( + { "git", "ls-files", "--ignored", "--exclude-standard", "--others", "--directory" }, + { + cwd = key, + text = true, + } + ) + local tracked_proc = vim.system({ "git", "ls-tree", "HEAD", "--name-only" }, { cwd = key, text = true, + }) + local ret = { + ignored = parse_output(ignore_proc), + tracked = parse_output(tracked_proc), } - ) - local result = proc:wait() - local ret = {} - if result.code == 0 then - for line in vim.gsplit(result.stdout, "\n", { plain = true, trimempty = true }) do - -- Remove trailing slash - line = line:gsub("/$", "") - table.insert(ret, line) - end - end - rawset(self, key, ret) - return ret - end, -}) + rawset(self, key, ret) + return ret + end, + }) +end +local git_status = new_git_status() + +-- Clear git status cache on refresh +local refresh = require("oil.actions").refresh +local orig_refresh = refresh.callback +refresh.callback = function(...) + git_status = new_git_status() + orig_refresh(...) +end require("oil").setup({ view_options = { is_hidden_file = function(name, _) - -- dotfiles are always considered hidden - if vim.startswith(name, ".") then - return true - end local dir = require("oil").get_current_dir() - -- if no local directory (e.g. for ssh connections), always show - if not dir then - return false + local is_dotfile = vim.startswith(name, ".") and name ~= ".." + -- if no local directory (e.g. for ssh connections), just hide dotfiles + if not dir then return is_dotfile end + -- dotfiles are considered hidden unless tracked + if is_dotfile then + return not git_status[dir].tracked[name] + else + -- Check if file is gitignored + return git_status[dir].ignored[name] end - -- Check if file is gitignored - return vim.list_contains(git_ignored[dir], name) end, }, }) From 0dc98d36b5450575e3af97fdfc6dcb72ba1fc80e Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 29 Aug 2024 16:26:36 +0000 Subject: [PATCH 072/206] [docgen] Update docs skip-checks: true --- README.md | 2 +- doc/recipes.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c1edd9..978b0fc 100644 --- a/README.md +++ b/README.md @@ -332,7 +332,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i ## Recipes - [Toggle file detail view](doc/recipes.md#toggle-file-detail-view) -- [Hide gitignored files](doc/recipes.md#hide-gitignored-files) +- [Hide gitignored files and show git tracked hidden files](doc/recipes.md#hide-gitignored-files-and-show-git-tracked-hidden-files) ## API diff --git a/doc/recipes.md b/doc/recipes.md index 525ddfc..7627a13 100644 --- a/doc/recipes.md +++ b/doc/recipes.md @@ -5,7 +5,7 @@ Have a cool recipe to share? Open a pull request and add it to this doc! - [Toggle file detail view](#toggle-file-detail-view) -- [Hide gitignored files](#hide-gitignored-files) +- [Hide gitignored files and show git tracked hidden files](#hide-gitignored-files-and-show-git-tracked-hidden-files) From d10e7f442f6cbc46107832dca20e47b21f8c9355 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 30 Aug 2024 17:10:53 -0700 Subject: [PATCH 073/206] doc: fix recipe for hiding gitignored files --- doc/recipes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/recipes.md b/doc/recipes.md index 7627a13..b2b875b 100644 --- a/doc/recipes.md +++ b/doc/recipes.md @@ -84,8 +84,8 @@ end require("oil").setup({ view_options = { - is_hidden_file = function(name, _) - local dir = require("oil").get_current_dir() + is_hidden_file = function(name, bufnr) + local dir = require("oil").get_current_dir(bufnr) local is_dotfile = vim.startswith(name, ".") and name ~= ".." -- if no local directory (e.g. for ssh connections), just hide dotfiles if not dir then return is_dotfile end From 1fe476daf0b3c108cb8ee1fc1226cc282fa2c9c1 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 30 Aug 2024 17:50:09 -0700 Subject: [PATCH 074/206] doc: more and better type annotations --- README.md | 2 + doc/api.md | 6 +- doc/oil.txt | 4 +- lua/oil/columns.lua | 2 +- lua/oil/config.lua | 152 ++++++++++++++++++++++++++++++++++++++++++++ lua/oil/init.lua | 2 +- lua/oil/layout.lua | 5 ++ lua/oil/types.lua | 42 ------------ lua/oil/view.lua | 2 +- 9 files changed, 167 insertions(+), 50 deletions(-) delete mode 100644 lua/oil/types.lua diff --git a/README.md b/README.md index 978b0fc..0728e9f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ oil.nvim supports all the usual plugin managers ```lua { 'stevearc/oil.nvim', + ---@module 'oil' + ---@type oil.SetupOpts opts = {}, -- Optional dependencies dependencies = { { "echasnovski/mini.icons", opts = {} } }, diff --git a/doc/api.md b/doc/api.md index 0018d5c..e107293 100644 --- a/doc/api.md +++ b/doc/api.md @@ -73,9 +73,9 @@ require("oil").set_sort({ { "type", "asc" }, { "size", "desc" } }) `set_is_hidden_file(is_hidden_file)` \ Change how oil determines if the file is hidden -| Param | Type | Desc | -| -------------- | ----------------------------------------------------- | -------------------------------------------- | -| is_hidden_file | `fun(filename: string, bufnr: nil\|integer): boolean` | Return true if the file/dir should be hidden | +| Param | Type | Desc | +| -------------- | ------------------------------------------------ | -------------------------------------------- | +| is_hidden_file | `fun(filename: string, bufnr: integer): boolean` | Return true if the file/dir should be hidden | ## toggle_hidden() diff --git a/doc/oil.txt b/doc/oil.txt index 9582cab..1877be3 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -270,8 +270,8 @@ set_is_hidden_file({is_hidden_file}) *oil.set_is_hidden_fil Change how oil determines if the file is hidden Parameters: - {is_hidden_file} `fun(filename: string, bufnr: nil|integer): boolean` Retu - rn true if the file/dir should be hidden + {is_hidden_file} `fun(filename: string, bufnr: integer): boolean` Return + true if the file/dir should be hidden toggle_hidden() *oil.toggle_hidden* Toggle hidden files and directories diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua index 97c6fcb..40b7d74 100644 --- a/lua/oil/columns.lua +++ b/lua/oil/columns.lua @@ -9,7 +9,7 @@ local FIELD_META = constants.FIELD_META local all_columns = {} ----@alias oil.ColumnSpec string|table +---@alias oil.ColumnSpec string|{[1]: string, [string]: any} ---@class (exact) oil.ColumnDefinition ---@field render fun(entry: oil.InternalEntry, conf: nil|table): nil|oil.TextChunk diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 3fef2e6..32da34e 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -192,8 +192,160 @@ default_config.adapters = { } default_config.adapter_aliases = {} +---@class oil.Config +---@field adapters table Hidden from SetupOpts +---@field adapter_aliases table Hidden from SetupOpts +---@field trash_command? string Deprecated option that we should clean up soon +---@field silence_scp_warning? boolean Undocumented option +---@field default_file_explorer boolean +---@field columns oil.ColumnSpec[] +---@field buf_options table +---@field win_options table +---@field delete_to_trash boolean +---@field skip_confirm_for_simple_edits boolean +---@field prompt_save_on_select_new_entry boolean +---@field cleanup_delay_ms integer +---@field lsp_file_methods oil.LspFileMethods +---@field constrain_cursor false|"name"|"editable" +---@field watch_for_changes boolean +---@field keymaps table +---@field use_default_keymaps boolean +---@field view_options oil.ViewOptions +---@field extra_scp_args string[] +---@field git oil.GitOptions +---@field float oil.FloatWindowConfig +---@field preview oil.PreviewWindowConfig +---@field progress oil.ProgressWindowConfig +---@field ssh oil.SimpleWindowConfig +---@field keymaps_help oil.SimpleWindowConfig local M = {} +-- For backwards compatibility +---@alias oil.setupOpts oil.SetupOpts + +---@class (exact) oil.SetupOpts +---@field default_file_explorer? boolean Oil will take over directory buffers (e.g. `vim .` or `:e src/`). Set to false if you still want to use netrw. +---@field columns? oil.ColumnSpec[] The columns to display. See :help oil-columns. +---@field buf_options? table Buffer-local options to use for oil buffers +---@field win_options? table Window-local options to use for oil buffers +---@field delete_to_trash? boolean Send deleted files to the trash instead of permanently deleting them (:help oil-trash). +---@field skip_confirm_for_simple_edits? boolean Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits). +---@field prompt_save_on_select_new_entry? boolean Selecting a new/moved/renamed file or directory will prompt you to save changes first (:help prompt_save_on_select_new_entry). +---@field cleanup_delay_ms? integer Oil will automatically delete hidden buffers after this delay. You can set the delay to false to disable cleanup entirely. Note that the cleanup process only starts when none of the oil buffers are currently displayed. +---@field lsp_file_methods? oil.SetupLspFileMethods Configure LSP file operation integration. +---@field constrain_cursor? false|"name"|"editable" Constrain the cursor to the editable parts of the oil buffer. Set to `false` to disable, or "name" to keep it on the file names. +---@field watch_for_changes? boolean Set to true to watch the filesystem for changes and reload oil. +---@field keymaps? table +---@field use_default_keymaps? boolean Set to false to disable all of the above keymaps +---@field view_options? oil.SetupViewOptions Configure which files are shown and how they are shown. +---@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH +---@field git? oil.SetupGitOptions EXPERIMENTAL support for performing file operations with git +---@field float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float +---@field preview? oil.SetupPreviewWindowConfig Configuration for the actions floating preview window +---@field progress? oil.SetupProgressWindowConfig Configuration for the floating progress window +---@field ssh? oil.SetupSimpleWindowConfig Configuration for the floating SSH window +---@field keymaps_help? oil.SetupSimpleWindowConfig Configuration for the floating keymaps help window + +---@class (exact) oil.LspFileMethods +---@field timeout_ms integer +---@field autosave_changes boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers. + +---@class (exact) oil.SetupLspFileMethods +---@field timeout_ms? integer Time to wait for LSP file operations to complete before skipping. +---@field autosave_changes? boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers. + +---@class (exact) oil.ViewOptions +---@field show_hidden boolean +---@field is_hidden_file fun(name: string, bufnr: integer): boolean +---@field is_always_hidden fun(name: string, bufnr: integer): boolean +---@field natural_order boolean +---@field case_insensitive boolean +---@field sort oil.SortSpec[] + +---@class (exact) oil.SetupViewOptions +---@field show_hidden? boolean Show files and directories that start with "." +---@field is_hidden_file? fun(name: string, bufnr: integer): boolean This function defines what is considered a "hidden" file +---@field is_always_hidden? fun(name: string, bufnr: integer): boolean This function defines what will never be shown, even when `show_hidden` is set +---@field natural_order? boolean Sort file names in a more intuitive order for humans. Is less performant, so you may want to set to false if you work with large directories. +---@field case_insensitive? boolean Sort file and directory names case insensitive +---@field sort? oil.SortSpec[] Sort order for the file list + +---@class (exact) oil.SortSpec +---@field [1] string +---@field [2] "asc"|"desc" + +---@class (exact) oil.GitOptions +---@field add fun(path: string): boolean +---@field mv fun(src_path: string, dest_path: string): boolean +---@field rm fun(path: string): boolean + +---@class (exact) oil.SetupGitOptions +---@field add? fun(path: string): boolean Return true to automatically git add a new file +---@field mv? fun(src_path: string, dest_path: string): boolean Return true to automatically git mv a moved file +---@field rm? fun(path: string): boolean Return true to automatically git rm a deleted file + +---@class (exact) oil.WindowDimensionDualConstraint +---@field [1] number +---@field [2] number + +---@alias oil.WindowDimension number|oil.WindowDimensionDualConstraint + +---@class (exact) oil.WindowConfig +---@field max_width oil.WindowDimension +---@field min_width oil.WindowDimension +---@field width? number +---@field max_height oil.WindowDimension +---@field min_height oil.WindowDimension +---@field height? number +---@field border string|string[] +---@field win_options table + +---@class (exact) oil.SetupWindowConfig +---@field max_width? oil.WindowDimension Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" +---@field min_width? oil.WindowDimension Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. min_width = {40, 0.4} means "the greater of 40 columns or 40% of total" +---@field width? number Define an integer/float for the exact width of the preview window +---@field max_height? oil.WindowDimension Height dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. max_height = {80, 0.9} means "the lesser of 80 columns or 90% of total" +---@field min_height? oil.WindowDimension Height dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%). Can be a single value or a list of mixed integer/float types. min_height = {5, 0.1} means "the greater of 5 columns or 10% of total" +---@field height? number Define an integer/float for the exact height of the preview window +---@field border? string|string[] Window border +---@field win_options? table + +---@class (exact) oil.PreviewWindowConfig : oil.WindowConfig +---@field update_on_cursor_moved boolean + +---@class (exact) oil.SetupPreviewWindowConfig : oil.SetupWindowConfig +---@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved + +---@class (exact) oil.ProgressWindowConfig : oil.WindowConfig +---@field minimized_border string|string[] + +---@class (exact) oil.SetupProgressWindowConfig : oil.SetupWindowConfig +---@field minimized_border? string|string[] The border for the minimized progress window + +---@class (exact) oil.FloatWindowConfig +---@field padding integer +---@field max_width integer +---@field max_height integer +---@field border string|string[] +---@field win_options table +---@field preview_split "auto"|"left"|"right"|"above"|"below" +---@field override fun(conf: table): table + +---@class (exact) oil.SetupFloatWindowConfig +---@field padding? integer +---@field max_width? integer +---@field max_height? integer +---@field border? string|string[] Window border +---@field win_options? table +---@field preview_split? "auto"|"left"|"right"|"above"|"below" Direction that the preview command will split the window +---@field override? fun(conf: table): table + +---@class (exact) oil.SimpleWindowConfig +---@field border string|string[] + +---@class (exact) oil.SetupSimpleWindowConfig +---@field border? string|string[] Window border + M.setup = function(opts) local new_conf = vim.tbl_deep_extend("keep", opts or {}, default_config) if not new_conf.use_default_keymaps then diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 73e3fe8..e8b4ddc 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -129,7 +129,7 @@ M.set_sort = function(sort) end ---Change how oil determines if the file is hidden ----@param is_hidden_file fun(filename: string, bufnr: nil|integer): boolean Return true if the file/dir should be hidden +---@param is_hidden_file fun(filename: string, bufnr: integer): boolean Return true if the file/dir should be hidden M.set_is_hidden_file = function(is_hidden_file) require("oil.view").set_is_hidden_file(is_hidden_file) end diff --git a/lua/oil/layout.lua b/lua/oil/layout.lua index 1ed257a..fef5f43 100644 --- a/lua/oil/layout.lua +++ b/lua/oil/layout.lua @@ -1,10 +1,15 @@ local M = {} +---@param value number +---@return boolean local function is_float(value) local _, p = math.modf(value) return p ~= 0 end +---@param value number +---@param max_value number +---@return number local function calc_float(value, max_value) if value and is_float(value) then return math.min(max_value, value * max_value) diff --git a/lua/oil/types.lua b/lua/oil/types.lua deleted file mode 100644 index ef6d26b..0000000 --- a/lua/oil/types.lua +++ /dev/null @@ -1,42 +0,0 @@ ----@class (exact) oil.setupOpts ----@field default_file_explorer? boolean Oil will take over directory buffers (e.g. `vim .` or `:e src/`). Set to false if you still want to use netrw. ----@field columns? oil.ColumnSpec[] The columns to display. See :help oil-columns. ----@field buf_options? table Buffer-local options to use for oil buffers ----@field win_options? table Window-local options to use for oil buffers ----@field delete_to_trash? boolean Send deleted files to the trash instead of permanently deleting them (:help oil-trash). ----@field skip_confirm_for_simple_edits? boolean Skip the confirmation popup for simple operations (:help oil.skip_confirm_for_simple_edits). ----@field prompt_save_on_select_new_entry? boolean Selecting a new/moved/renamed file or directory will prompt you to save changes first (:help prompt_save_on_select_new_entry). ----@field cleanup_delay_ms? integer Oil will automatically delete hidden buffers after this delay. You can set the delay to false to disable cleanup entirely. Note that the cleanup process only starts when none of the oil buffers are currently displayed. ----@field lsp_file_methods? oil.LspFileMethods Configure LSP file operation integration. ----@field constrain_cursor? false|"name"|"editable" Constrain the cursor to the editable parts of the oil buffer. Set to `false` to disable, or "name" to keep it on the file names. ----@field watch_for_changes? boolean Set to true to watch the filesystem for changes and reload oil. ----@field keymaps? table ----@field use_default_keymaps? boolean Set to false to disable all of the above keymaps ----@field view_options? oil.ViewOptions Configure which files are shown and how they are shown. ----@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH ----@field git? oil.GitOptions EXPERIMENTAL support for performing file operations with git ----@field float? table Configuration for the floating window in oil.open_float ----@field preview? table Configuration for the actions floating preview window ----@field progress? table Configuration for the floating progress window ----@field ssh? table Configuration for the floating SSH window ----@field keymaps_help? table Configuration for the floating keymaps help window - ----@class (exact) oil.LspFileMethods ----@field timeout_ms? integer Time to wait for LSP file operations to complete before skipping. ----@field autosave_changes? boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers. - ----@class (exact) oil.ViewOptions ----@field show_hidden? boolean Show files and directories that start with "." ----@field is_hidden_file? fun(name: string, bufnr: integer): boolean This function defines what is considered a "hidden" file ----@field is_always_hidden? fun(name: string, bufnr: integer): boolean This function defines what will never be shown, even when `show_hidden` is set ----@field natural_order? boolean Sort file names in a more intuitive order for humans. Is less performant, so you may want to set to false if you work with large directories. ----@field sort? oil.SortSpec[] Sort order for the file list - ----@class (exact) oil.SortSpec ----@field [1] string ----@field [2] "asc"|"desc" - ----@class (exact) oil.GitOptions ----@field add? fun(path: string): boolean Return true to automatically git add a new file ----@field mv? fun(src_path: string, dest_path: string): boolean Return true to automatically git mv a moved file ----@field rm? fun(path: string): boolean Return true to automatically git rm a deleted file diff --git a/lua/oil/view.lua b/lua/oil/view.lua index 658bbad..89e4b8f 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -79,7 +79,7 @@ M.toggle_hidden = function() end end ----@param is_hidden_file fun(filename: string, bufnr: nil|integer): boolean +---@param is_hidden_file fun(filename: string, bufnr: integer): boolean M.set_is_hidden_file = function(is_hidden_file) local any_modified = are_any_modified() if any_modified then From b05374428e5136d9b6c8e1e8e62a75f82283b1f8 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Tue, 10 Sep 2024 11:44:04 -0700 Subject: [PATCH 075/206] fix: wrap git rm callback in schedule_wrap (#475) --- lua/oil/adapters/files.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua index e007423..f65cbf7 100644 --- a/lua/oil/adapters/files.lua +++ b/lua/oil/adapters/files.lua @@ -554,13 +554,13 @@ M.perform_action = function(action, cb) if config.git.rm(path) then local old_cb = cb - cb = function(err) + cb = vim.schedule_wrap(function(err) if not err then git.rm(path, old_cb) else old_cb(err) end - end + end) end if config.delete_to_trash then From 1eb9fb35a4613518f79790117ed82c367a0b4a22 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:03:08 -0700 Subject: [PATCH 076/206] chore(master): release 2.12.2 (#472) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c68e2f..39811f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.12.2](https://github.com/stevearc/oil.nvim/compare/v2.12.1...v2.12.2) (2024-09-10) + + +### Bug Fixes + +* ensure win_options are being set on correct window ([#469](https://github.com/stevearc/oil.nvim/issues/469)) ([30e0438](https://github.com/stevearc/oil.nvim/commit/30e0438ff08f197d7ce4a417445ab97ee72efe2d)) +* wrap git rm callback in schedule_wrap ([#475](https://github.com/stevearc/oil.nvim/issues/475)) ([b053744](https://github.com/stevearc/oil.nvim/commit/b05374428e5136d9b6c8e1e8e62a75f82283b1f8)) + + +### Performance Improvements + +* **view:** avoid running `is_hidden_file` when `show_hidden` is set ([#471](https://github.com/stevearc/oil.nvim/issues/471)) ([0fcd126](https://github.com/stevearc/oil.nvim/commit/0fcd1263a2e8b6200e2b9fd4ab83d40ed8899c54)) + ## [2.12.1](https://github.com/stevearc/oil.nvim/compare/v2.12.0...v2.12.1) (2024-08-26) From eadc3ed42ee2299cccce88cffafab8498401ffbd Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Wed, 11 Sep 2024 08:44:45 -0700 Subject: [PATCH 077/206] doc: add recipe to show current directory in the winbar --- doc/recipes.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/doc/recipes.md b/doc/recipes.md index b2b875b..59d65a4 100644 --- a/doc/recipes.md +++ b/doc/recipes.md @@ -30,6 +30,27 @@ require("oil").setup({ }) ``` +## Show CWD in the winbar + +```lua +-- Declare a global function to retrieve the current directory +function _G.get_oil_winbar() + local dir = require("oil").get_current_dir() + if dir then + return vim.fn.fnamemodify(dir, ":~") + else + -- If there is no current directory (e.g. over ssh), just show the buffer name + return vim.api.nvim_buf_get_name(0) + end +end + +require("oil").setup({ + win_options = { + winbar = "%!v:lua.get_oil_winbar()", + }, +}) +``` + ## Hide gitignored files and show git tracked hidden files ```lua @@ -88,7 +109,9 @@ require("oil").setup({ local dir = require("oil").get_current_dir(bufnr) local is_dotfile = vim.startswith(name, ".") and name ~= ".." -- if no local directory (e.g. for ssh connections), just hide dotfiles - if not dir then return is_dotfile end + if not dir then + return is_dotfile + end -- dotfiles are considered hidden unless tracked if is_dotfile then return not git_status[dir].tracked[name] From 665bf2edc9f9a6d77a6fc636df36276c97fb5d66 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 11 Sep 2024 15:45:10 +0000 Subject: [PATCH 078/206] [docgen] Update docs skip-checks: true --- README.md | 1 + doc/recipes.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 0728e9f..9679fd7 100644 --- a/README.md +++ b/README.md @@ -334,6 +334,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i ## Recipes - [Toggle file detail view](doc/recipes.md#toggle-file-detail-view) +- [Show CWD in the winbar](doc/recipes.md#show-cwd-in-the-winbar) - [Hide gitignored files and show git tracked hidden files](doc/recipes.md#hide-gitignored-files-and-show-git-tracked-hidden-files) ## API diff --git a/doc/recipes.md b/doc/recipes.md index 59d65a4..f32fd6e 100644 --- a/doc/recipes.md +++ b/doc/recipes.md @@ -5,6 +5,7 @@ Have a cool recipe to share? Open a pull request and add it to this doc! - [Toggle file detail view](#toggle-file-detail-view) +- [Show CWD in the winbar](#show-cwd-in-the-winbar) - [Hide gitignored files and show git tracked hidden files](#hide-gitignored-files-and-show-git-tracked-hidden-files) From f60bb7f793477d99ef1acf39e920bf2ca4e644de Mon Sep 17 00:00:00 2001 From: csponge Date: Wed, 11 Sep 2024 22:41:46 -0400 Subject: [PATCH 079/206] feat: config option to disable lsp file methods (#477) * added config option to enable or disable lsp_file_methods * refactor: rename enable -> enabled --------- Co-authored-by: Steven Arcangeli --- README.md | 2 ++ doc/oil.txt | 2 ++ lua/oil/config.lua | 4 ++++ lua/oil/mutator/init.lua | 10 ++++++++-- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9679fd7..606cbb2 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,8 @@ require("oil").setup({ -- Note that the cleanup process only starts when none of the oil buffers are currently displayed cleanup_delay_ms = 2000, lsp_file_methods = { + -- Enable or disable LSP file operations + enabled = true, -- Time to wait for LSP file operations to complete before skipping timeout_ms = 1000, -- Set to true to autosave buffers that are updated with LSP willRenameFiles diff --git a/doc/oil.txt b/doc/oil.txt index 1877be3..d028480 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -55,6 +55,8 @@ CONFIG *oil-confi -- Note that the cleanup process only starts when none of the oil buffers are currently displayed cleanup_delay_ms = 2000, lsp_file_methods = { + -- Enable or disable LSP file operations + enabled = true, -- Time to wait for LSP file operations to complete before skipping timeout_ms = 1000, -- Set to true to autosave buffers that are updated with LSP willRenameFiles diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 32da34e..e3fcc10 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -40,6 +40,8 @@ local default_config = { -- Note that the cleanup process only starts when none of the oil buffers are currently displayed cleanup_delay_ms = 2000, lsp_file_methods = { + -- Enable or disable LSP file operations + enabled = true, -- Time to wait for LSP file operations to complete before skipping timeout_ms = 1000, -- Set to true to autosave buffers that are updated with LSP willRenameFiles @@ -247,10 +249,12 @@ local M = {} ---@field keymaps_help? oil.SetupSimpleWindowConfig Configuration for the floating keymaps help window ---@class (exact) oil.LspFileMethods +---@field enabled boolean ---@field timeout_ms integer ---@field autosave_changes boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers. ---@class (exact) oil.SetupLspFileMethods +---@field enabled? boolean Enable or disable LSP file operations ---@field timeout_ms? integer Time to wait for LSP file operations to complete before skipping. ---@field autosave_changes? boolean|"unmodified" Set to true to autosave buffers that are updated with LSP willRenameFiles. Set to "unmodified" to only save unmodified buffers. diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index ab24a91..8b72a01 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -389,7 +389,11 @@ M.process_actions = function(actions, cb) "User", { pattern = "OilActionsPre", modeline = false, data = { actions = actions } } ) - local did_complete = lsp_helpers.will_perform_file_operations(actions) + + local did_complete = nil + if config.lsp_file_methods.enabled then + did_complete = lsp_helpers.will_perform_file_operations(actions) + end -- Convert some cross-adapter moves to a copy + delete for _, action in ipairs(actions) do @@ -442,7 +446,9 @@ M.process_actions = function(actions, cb) return end if idx > #actions then - did_complete() + if did_complete then + did_complete() + end finish() return end From 1360be5fda9c67338331abfcd80de2afbb395bcd Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 30 Aug 2024 17:09:56 -0700 Subject: [PATCH 080/206] lint: stricter type checking --- .luarc.json | 9 +++++++++ lua/oil/adapters/files.lua | 5 +++++ lua/oil/adapters/ssh.lua | 2 ++ lua/oil/adapters/ssh/connection.lua | 1 + lua/oil/adapters/ssh/sshfs.lua | 1 + lua/oil/adapters/trash/freedesktop.lua | 1 + lua/oil/adapters/trash/windows/powershell-connection.lua | 1 + lua/oil/layout.lua | 7 ++++--- lua/oil/mutator/trie.lua | 1 + 9 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 .luarc.json diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..68da2f2 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,9 @@ +{ + "runtime": { + "version": "LuaJIT", + "pathStrict": true + }, + "type": { + "checkTableShape": true + } +} diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua index f65cbf7..593cfce 100644 --- a/lua/oil/adapters/files.lua +++ b/lua/oil/adapters/files.lua @@ -34,6 +34,9 @@ local function read_link_data(path, cb) ) end +---@class (exact) oil.FilesAdapter: oil.Adapter +---@field to_short_os_path fun(path: string, entry_type: nil|oil.EntryType): string + ---@param path string ---@param entry_type nil|oil.EntryType ---@return string @@ -284,6 +287,7 @@ end ---@param column_defs string[] ---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun()) local function list_windows_drives(url, column_defs, cb) + ---@cast M oil.FilesAdapter local fetch_meta = columns.get_metadata_fetcher(M, column_defs) local stdout = "" local jid = vim.fn.jobstart({ "wmic", "logicaldisk", "get", "name" }, { @@ -341,6 +345,7 @@ M.list = function(url, column_defs, cb) return list_windows_drives(url, column_defs, cb) end local dir = fs.posix_to_os_path(path) + ---@cast M oil.Adapter local fetch_meta = columns.get_metadata_fetcher(M, column_defs) ---@diagnostic disable-next-line: param-type-mismatch, discard-returns diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua index c52f5a9..0b619ae 100644 --- a/lua/oil/adapters/ssh.lua +++ b/lua/oil/adapters/ssh.lua @@ -50,6 +50,7 @@ M.parse_url = function(oil_url) error(string.format("Malformed SSH url: %s", oil_url)) end + ---@cast ret oil.sshUrl return ret end @@ -440,6 +441,7 @@ M.goto_file = function() url.path = vim.fs.dirname(fullpath) local parurl = url_to_str(url) + ---@cast M oil.Adapter util.adapter_list_all(M, parurl, {}, function(err, entries) if err then vim.notify(string.format("Error finding file '%s': %s", fname, err), vim.log.levels.ERROR) diff --git a/lua/oil/adapters/ssh/connection.lua b/lua/oil/adapters/ssh/connection.lua index 146b140..6a47c07 100644 --- a/lua/oil/adapters/ssh/connection.lua +++ b/lua/oil/adapters/ssh/connection.lua @@ -176,6 +176,7 @@ function SSHConnection.new(url) end end) + ---@cast self oil.sshConnection return self end diff --git a/lua/oil/adapters/ssh/sshfs.lua b/lua/oil/adapters/ssh/sshfs.lua index 3b9faf3..341834c 100644 --- a/lua/oil/adapters/ssh/sshfs.lua +++ b/lua/oil/adapters/ssh/sshfs.lua @@ -70,6 +70,7 @@ end ---@param url oil.sshUrl ---@return oil.sshFs function SSHFS.new(url) + ---@type oil.sshFs return setmetatable({ conn = SSHConnection.new(url), }, { diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua index c6221ab..4a65885 100644 --- a/lua/oil/adapters/trash/freedesktop.lua +++ b/lua/oil/adapters/trash/freedesktop.lua @@ -210,6 +210,7 @@ local function read_trash_info(info_file, cb) cb(".trashinfo file points to non-existant file") else trash_info.stat = trash_stat + ---@cast trash_info oil.TrashInfo cb(nil, trash_info) end end) diff --git a/lua/oil/adapters/trash/windows/powershell-connection.lua b/lua/oil/adapters/trash/windows/powershell-connection.lua index f16c346..a296a7e 100644 --- a/lua/oil/adapters/trash/windows/powershell-connection.lua +++ b/lua/oil/adapters/trash/windows/powershell-connection.lua @@ -22,6 +22,7 @@ function PowershellConnection.new(init_command) self:_init(init_command) + ---@type oil.PowershellConnection return self end diff --git a/lua/oil/layout.lua b/lua/oil/layout.lua index fef5f43..f22d26a 100644 --- a/lua/oil/layout.lua +++ b/lua/oil/layout.lua @@ -98,7 +98,7 @@ M.calculate_height = function(desired_height, opts) ) end ----@class (exact) conform.WinLayout +---@class (exact) oil.WinLayout ---@field width integer ---@field height integer ---@field row integer @@ -139,14 +139,15 @@ end ---@param winid integer ---@param direction "above"|"below"|"left"|"right"|"auto" ---@param gap integer ----@return conform.WinLayout root_dim New dimensions of the original window ----@return conform.WinLayout new_dim New dimensions of the new window +---@return oil.WinLayout root_dim New dimensions of the original window +---@return oil.WinLayout new_dim New dimensions of the new window M.split_window = function(winid, direction, gap) if direction == "auto" then direction = vim.o.splitright and "right" or "left" end local float_config = vim.api.nvim_win_get_config(winid) + ---@type oil.WinLayout local dim_root = { width = float_config.width, height = float_config.height, diff --git a/lua/oil/mutator/trie.lua b/lua/oil/mutator/trie.lua index ea39266..7bac161 100644 --- a/lua/oil/mutator/trie.lua +++ b/lua/oil/mutator/trie.lua @@ -7,6 +7,7 @@ local Trie = {} ---@return oil.Trie Trie.new = function() + ---@type oil.Trie return setmetatable({ root = { values = {}, children = {} }, }, { From 581c7298053f26e8a33be69854c194291951ced3 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Mon, 30 Sep 2024 22:10:36 -0700 Subject: [PATCH 081/206] doc: disable some type warnings from new LuaLS release --- lua/oil/cache.lua | 1 + lua/oil/mutator/init.lua | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lua/oil/cache.lua b/lua/oil/cache.lua index ef78246..ef5597c 100644 --- a/lua/oil/cache.lua +++ b/lua/oil/cache.lua @@ -197,6 +197,7 @@ M.perform_action = function(action) elseif action.type == "change" then -- Cache doesn't need to update else + ---@diagnostic disable-next-line: undefined-field error(string.format("Bad action type: '%s'", action.type)) end end diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index 8b72a01..8dfd4b0 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -140,6 +140,7 @@ M.create_actions_from_diffs = function(all_diffs) else local by_id = diff_by_id[diff.id] -- HACK: set has_delete field on a list-like table of diffs + ---@diagnostic disable-next-line: inject-field by_id.has_delete = true -- Don't insert the delete. We already know that there is a delete because of the presence -- in the diff_by_id map. The list will only include the 'new' diffs. @@ -401,6 +402,7 @@ M.process_actions = function(actions, cb) local _, cross_action = util.get_adapter_for_action(action) -- Only do the conversion if the cross-adapter support is "copy" if cross_action == "copy" then + ---@diagnostic disable-next-line: assign-type-mismatch action.type = "copy" table.insert(actions, { type = "delete", From 9e6fb844fe4cce13d9e3f6a7c970cbc669bb1629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=CC=81ric=20NICOLAS?= Date: Tue, 1 Oct 2024 07:11:09 +0200 Subject: [PATCH 082/206] doc: Update links to FreeDesktop's Trash spec (#490) Keeping it pinned to 1.0 Fixes #489 --- doc/oil.txt | 2 +- lua/oil/adapters/trash/freedesktop.lua | 2 +- scripts/generate.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/oil.txt b/doc/oil.txt index d028480..4364ebb 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -640,7 +640,7 @@ trash they will be permanently deleted (purged). Linux: Oil supports the FreeDesktop trash specification. - https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html + https://specifications.freedesktop.org/trash-spec/1.0/ All features should work. Mac: diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua index 4a65885..ac45492 100644 --- a/lua/oil/adapters/trash/freedesktop.lua +++ b/lua/oil/adapters/trash/freedesktop.lua @@ -1,5 +1,5 @@ -- Based on the FreeDesktop.org trash specification --- https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html +-- https://specifications.freedesktop.org/trash-spec/1.0/ local cache = require("oil.cache") local config = require("oil.config") local constants = require("oil.constants") diff --git a/scripts/generate.py b/scripts/generate.py index 8875e9f..5cc5fe3 100755 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -357,7 +357,7 @@ trash they will be permanently deleted (purged). Linux: Oil supports the FreeDesktop trash specification. - https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html + https://specifications.freedesktop.org/trash-spec/1.0/ All features should work. Mac: From ccab9d5e09e2d0042fbbe5b6bd05e82426247067 Mon Sep 17 00:00:00 2001 From: staticssleever668 Date: Fri, 4 Oct 2024 06:51:55 +0300 Subject: [PATCH 083/206] fix: only map ~ for normal mode (#484) Allows to switch character case with ~ (tilde) in visual mode while preserving existing ~ :tcd functionality. Related to [1]. [1]: https://github.com/stevearc/oil.nvim/issues/397 "bug: ~ not respected" --- README.md | 2 +- doc/oil.txt | 2 +- lua/oil/config.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 606cbb2..fd15360 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ require("oil").setup({ ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" }, + ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory", mode = "n" }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", diff --git a/doc/oil.txt b/doc/oil.txt index 4364ebb..2c1a107 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -86,7 +86,7 @@ CONFIG *oil-confi ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" }, + ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory", mode = "n" }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", diff --git a/lua/oil/config.lua b/lua/oil/config.lua index e3fcc10..065dbbf 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -71,7 +71,7 @@ local default_config = { ["-"] = "actions.parent", ["_"] = "actions.open_cwd", ["`"] = "actions.cd", - ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory" }, + ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory", mode = "n" }, ["gs"] = "actions.change_sort", ["gx"] = "actions.open_external", ["g."] = "actions.toggle_hidden", From 5d2dfae655b9b689bd4017b3bdccd52cbee5b92f Mon Sep 17 00:00:00 2001 From: Philipp Oeschger <49947694+PhilippOesch@users.noreply.github.com> Date: Wed, 16 Oct 2024 04:22:31 +0200 Subject: [PATCH 084/206] feat: config option to customize floating window title (#482) * replace cwd path in actual path * move get_title to utils * add documentation * rename * add method doc * add comment * fallback to 0 for winid * add missing property definition for relative_win_title * only replace when at the start of the path * simplify * minor change * add entry point to customize floating window title for oil-buffer * remove config parameter * cleanup * add documentation * move get_win_title to top and pass winid as parameter * add get_win_title to type definition for oil.setup * remove empty line * adjust comment --------- Co-authored-by: Philipp Oeschger --- doc/oil.txt | 2 ++ lua/oil/config.lua | 4 ++++ lua/oil/init.lua | 15 +-------------- lua/oil/util.lua | 35 ++++++++++++++++++++++------------- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/doc/oil.txt b/doc/oil.txt index 2c1a107..43c52d2 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -142,6 +142,8 @@ CONFIG *oil-confi win_options = { winblend = 0, }, + -- optionally override the oil buffers window title with custom function: fun(path: string): string + get_win_title = nil, -- preview_split: Split direction: "auto", "left", "right", "above", "below". preview_split = "auto", -- This is the config that will be passed to nvim_open_win. diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 065dbbf..5ccde17 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -127,6 +127,8 @@ local default_config = { win_options = { winblend = 0, }, + -- optionally override the oil buffers window title with custom function: fun(winid: integer): string + get_win_title = nil, -- preview_split: Split direction: "auto", "left", "right", "above", "below". preview_split = "auto", -- This is the config that will be passed to nvim_open_win. @@ -332,6 +334,7 @@ local M = {} ---@field max_height integer ---@field border string|string[] ---@field win_options table +---@field get_win_title fun(winid: integer): string ---@field preview_split "auto"|"left"|"right"|"above"|"below" ---@field override fun(conf: table): table @@ -341,6 +344,7 @@ local M = {} ---@field max_height? integer ---@field border? string|string[] Window border ---@field win_options? table +---@field get_win_title? fun(winid: integer): string ---@field preview_split? "auto"|"left"|"right"|"above"|"below" Direction that the preview command will split the window ---@field override? fun(conf: table): table diff --git a/lua/oil/init.lua b/lua/oil/init.lua index e8b4ddc..8c04d8b 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -288,19 +288,6 @@ M.open_float = function(dir) }) ) - ---Recalculate the window title for the current buffer - local function get_title() - local src_buf = vim.api.nvim_win_get_buf(winid) - local title = vim.api.nvim_buf_get_name(src_buf) - local scheme, path = util.parse_url(title) - if config.adapters[scheme] == "files" then - assert(path) - local fs = require("oil.fs") - title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") - end - return title - end - table.insert( autocmds, vim.api.nvim_create_autocmd("BufWinEnter", { @@ -324,7 +311,7 @@ M.open_float = function(dir) col = cur_win_opts.col, width = cur_win_opts.width, height = cur_win_opts.height, - title = get_title(), + title = util.get_title(winid), }) end end, diff --git a/lua/oil/util.lua b/lua/oil/util.lua index aa717ad..f5c63e2 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -360,6 +360,26 @@ M.is_floating_win = function(winid) return vim.api.nvim_win_get_config(winid or 0).relative ~= "" end +---Recalculate the window title for the current buffer +---@param winid nil|integer +---@return string +M.get_title = function(winid) + if config.float.get_win_title ~= nil then + return config.float.get_win_title(winid or 0) + end + + local src_buf = vim.api.nvim_win_get_buf(winid or 0) + local title = vim.api.nvim_buf_get_name(src_buf) + local scheme, path = M.parse_url(title) + + if config.adapters[scheme] == "files" then + assert(path) + local fs = require("oil.fs") + title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") + end + return title +end + local winid_map = {} M.add_title_to_win = function(winid, opts) opts = opts or {} @@ -367,21 +387,10 @@ M.add_title_to_win = function(winid, opts) if not vim.api.nvim_win_is_valid(winid) then return end - local function get_title() - local src_buf = vim.api.nvim_win_get_buf(winid) - local title = vim.api.nvim_buf_get_name(src_buf) - local scheme, path = M.parse_url(title) - if config.adapters[scheme] == "files" then - assert(path) - local fs = require("oil.fs") - title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ":~") - end - return title - end -- HACK to force the parent window to position itself -- See https://github.com/neovim/neovim/issues/13403 vim.cmd.redraw() - local title = get_title() + local title = M.get_title(winid) local width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(title)) local title_winid = winid_map[winid] local bufnr @@ -429,7 +438,7 @@ M.add_title_to_win = function(winid, opts) if vim.api.nvim_win_get_buf(winid) ~= winbuf then return end - local new_title = get_title() + local new_title = M.get_title(winid) local new_width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(new_title)) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. new_title .. " " }) From 39dbf875861449cf09e936fa80073f3413e9439c Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 16 Oct 2024 02:22:43 +0000 Subject: [PATCH 085/206] [docgen] Update docs skip-checks: true --- README.md | 2 ++ doc/oil.txt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fd15360..46063b2 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,8 @@ require("oil").setup({ win_options = { winblend = 0, }, + -- optionally override the oil buffers window title with custom function: fun(winid: integer): string + get_win_title = nil, -- preview_split: Split direction: "auto", "left", "right", "above", "below". preview_split = "auto", -- This is the config that will be passed to nvim_open_win. diff --git a/doc/oil.txt b/doc/oil.txt index 43c52d2..8e96cf4 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -142,7 +142,7 @@ CONFIG *oil-confi win_options = { winblend = 0, }, - -- optionally override the oil buffers window title with custom function: fun(path: string): string + -- optionally override the oil buffers window title with custom function: fun(winid: integer): string get_win_title = nil, -- preview_split: Split direction: "auto", "left", "right", "above", "below". preview_split = "auto", From 28aca0c1f50e5a40eea472eea302ce4cd035ea65 Mon Sep 17 00:00:00 2001 From: Yu Guo Date: Sat, 26 Oct 2024 01:07:11 +0900 Subject: [PATCH 086/206] chore: add __pycache__ to gitignore (#498) * add __pycache__ to gitignore * doc: fix typo --------- Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com> --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d8cb86e..290f656 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ luac.out *.zip *.tar.gz +# python bytecode +__pycache__ + # Object files *.o *.os From cca1631d5ea450c09ba72f3951a9e28105a3632c Mon Sep 17 00:00:00 2001 From: Yu Guo Date: Sat, 26 Oct 2024 01:08:39 +0900 Subject: [PATCH 087/206] fix: actions.preview accepts options (#497) * fix: pass opts to actions.preview * add opts type to action.preview * run generate.py script --- doc/oil.txt | 6 ++++++ lua/oil/actions.lua | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/doc/oil.txt b/doc/oil.txt index 8e96cf4..70c589d 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -527,6 +527,12 @@ preview *actions.previe Open the entry under the cursor in a preview window, or close the preview window if already open + Parameters: + {horizontal} `boolean` Open the buffer in a horizontal split + {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + modifier + {vertical} `boolean` Open the buffer in a vertical split + preview_scroll_down *actions.preview_scroll_down* Scroll down in the preview window diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 6a2a5ff..b097e68 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -69,7 +69,21 @@ M.select_tab = { M.preview = { desc = "Open the entry under the cursor in a preview window, or close the preview window if already open", - callback = function() + parameters = { + vertical = { + type = "boolean", + desc = "Open the buffer in a vertical split", + }, + horizontal = { + type = "boolean", + desc = "Open the buffer in a horizontal split", + }, + split = { + type = '"aboveleft"|"belowright"|"topleft"|"botright"', + desc = "Split modifier", + }, + }, + callback = function(opts) local entry = oil.get_cursor_entry() if not entry then vim.notify("Could not find entry under cursor", vim.log.levels.ERROR) @@ -88,7 +102,7 @@ M.preview = { return end end - oil.open_preview() + oil.open_preview(opts) end, } From 42333bb46e34dd47e13927010b1dcd30e6e4ca96 Mon Sep 17 00:00:00 2001 From: Foo-x Date: Tue, 29 Oct 2024 02:55:25 +0900 Subject: [PATCH 088/206] fix: add trailing slash to directories on yank_entry (#504) * feat: add trailing slash on yank_entry Closes #503 * style: format --- lua/oil/actions.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index b097e68..50a266a 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -366,7 +366,11 @@ M.yank_entry = { if not entry or not dir then return end - local path = dir .. entry.name + local name = entry.name + if entry.type == "directory" then + name = name .. "/" + end + local path = dir .. name if opts.modify then path = vim.fn.fnamemodify(path, opts.modify) end From 52cc8a1fb35ea6ce1df536143add7ce7215c63c0 Mon Sep 17 00:00:00 2001 From: Foo-x Date: Thu, 31 Oct 2024 00:53:36 +0900 Subject: [PATCH 089/206] fix: sort keymap help entries by description (#506) Closes #376 --- lua/oil/keymap_util.lua | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua index b94581c..b62756e 100644 --- a/lua/oil/keymap_util.lua +++ b/lua/oil/keymap_util.lua @@ -78,31 +78,30 @@ M.show_help = function(keymaps) end end - local col_left = {} - local col_desc = {} local max_lhs = 1 + local keymap_entries = {} for k, rhs in pairs(keymaps) do local all_lhs = lhs_to_all_lhs[k] if all_lhs then local _, opts = resolve(rhs) local keystr = table.concat(all_lhs, "/") max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr)) - table.insert(col_left, { str = keystr, all_lhs = all_lhs }) - table.insert(col_desc, opts.desc or "") + table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or "" }) end end + table.sort(keymap_entries, function(a, b) + return a.desc < b.desc + end) local lines = {} local highlights = {} local max_line = 1 - for i = 1, #col_left do - local left = col_left[i] - local desc = col_desc[i] - local line = string.format(" %s %s", util.rpad(left.str, max_lhs), desc) + for _, entry in ipairs(keymap_entries) do + local line = string.format(" %s %s", util.rpad(entry.str, max_lhs), entry.desc) max_line = math.max(max_line, vim.api.nvim_strwidth(line)) table.insert(lines, line) local start = 1 - for _, key in ipairs(left.all_lhs) do + for _, key in ipairs(entry.all_lhs) do local keywidth = vim.api.nvim_strwidth(key) table.insert(highlights, { "Special", #lines, start, start + keywidth }) start = start + keywidth + 1 From 709403ccd6f22d859c2e42c780ab558ae89284d9 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sat, 9 Nov 2024 22:28:24 -0800 Subject: [PATCH 090/206] fix: don't deep merge keymaps (#510) --- lua/oil/config.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 5ccde17..2505c3e 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -355,9 +355,16 @@ local M = {} ---@field border? string|string[] Window border M.setup = function(opts) - local new_conf = vim.tbl_deep_extend("keep", opts or {}, default_config) + opts = opts or {} + local new_conf = vim.tbl_deep_extend("keep", opts, default_config) if not new_conf.use_default_keymaps then new_conf.keymaps = opts.keymaps or {} + else + -- We don't want to deep merge the keymaps, we want any keymap defined by the user to override + -- everything about the default. + for k, v in pairs(opts.keymaps) do + new_conf.keymaps[k] = v + end end if new_conf.lsp_rename_autosave ~= nil then From 621f8ba4fa821724e9b646732a26fb2e795fe008 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sat, 9 Nov 2024 22:31:35 -0800 Subject: [PATCH 091/206] fix: guard against nil keymaps --- lua/oil/config.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 2505c3e..0c4f130 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -359,7 +359,7 @@ M.setup = function(opts) local new_conf = vim.tbl_deep_extend("keep", opts, default_config) if not new_conf.use_default_keymaps then new_conf.keymaps = opts.keymaps or {} - else + elseif opts.keymaps then -- We don't want to deep merge the keymaps, we want any keymap defined by the user to override -- everything about the default. for k, v in pairs(opts.keymaps) do From 1f5b002270addb9010740e34824244e777de7c9e Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 10 Nov 2024 15:51:46 -0800 Subject: [PATCH 092/206] refactor: rename action preview window to 'confirmation' window --- README.md | 9 ++++--- doc/oil.txt | 9 ++++--- lua/oil/config.lua | 27 ++++++++++++++----- lua/oil/layout.lua | 5 ++++ .../mutator/{preview.lua => confirmation.lua} | 8 +++--- lua/oil/mutator/init.lua | 4 +-- lua/oil/view.lua | 2 +- 7 files changed, 45 insertions(+), 19 deletions(-) rename lua/oil/mutator/{preview.lua => confirmation.lua} (97%) diff --git a/README.md b/README.md index 46063b2..4d91f1d 100644 --- a/README.md +++ b/README.md @@ -267,8 +267,13 @@ require("oil").setup({ return conf end, }, - -- Configuration for the actions floating preview window + -- Configuration for the file preview window preview = { + -- Whether the preview window is automatically updated when the cursor is moved + update_on_cursor_moved = true, + }, + -- Configuration for the floating action confirmation window + confirmation = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -289,8 +294,6 @@ require("oil").setup({ win_options = { winblend = 0, }, - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, }, -- Configuration for the floating progress window progress = { diff --git a/doc/oil.txt b/doc/oil.txt index 70c589d..cafd61e 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -152,8 +152,13 @@ CONFIG *oil-confi return conf end, }, - -- Configuration for the actions floating preview window + -- Configuration for the file preview window preview = { + -- Whether the preview window is automatically updated when the cursor is moved + update_on_cursor_moved = true, + }, + -- Configuration for the floating action confirmation window + confirmation = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -174,8 +179,6 @@ CONFIG *oil-confi win_options = { winblend = 0, }, - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, }, -- Configuration for the floating progress window progress = { diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 0c4f130..bcb7225 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -137,8 +137,13 @@ local default_config = { return conf end, }, - -- Configuration for the actions floating preview window + -- Configuration for the file preview window preview = { + -- Whether the preview window is automatically updated when the cursor is moved + update_on_cursor_moved = true, + }, + -- Configuration for the floating action confirmation window + confirmation = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -159,8 +164,6 @@ local default_config = { win_options = { winblend = 0, }, - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, }, -- Configuration for the floating progress window progress = { @@ -219,6 +222,7 @@ default_config.adapter_aliases = {} ---@field git oil.GitOptions ---@field float oil.FloatWindowConfig ---@field preview oil.PreviewWindowConfig +---@field confirmation oil.ConfirmationWindowConfig ---@field progress oil.ProgressWindowConfig ---@field ssh oil.SimpleWindowConfig ---@field keymaps_help oil.SimpleWindowConfig @@ -245,7 +249,8 @@ local M = {} ---@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH ---@field git? oil.SetupGitOptions EXPERIMENTAL support for performing file operations with git ---@field float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float ----@field preview? oil.SetupPreviewWindowConfig Configuration for the actions floating preview window +---@field preview? oil.SetupPreviewWindowConfig Configuration for the file preview window +---@field confirmation? oil.SetupConfirmationWindowConfig Configuration for the floating action confirmation window ---@field progress? oil.SetupProgressWindowConfig Configuration for the floating progress window ---@field ssh? oil.SetupSimpleWindowConfig Configuration for the floating SSH window ---@field keymaps_help? oil.SetupSimpleWindowConfig Configuration for the floating keymaps help window @@ -316,12 +321,16 @@ local M = {} ---@field border? string|string[] Window border ---@field win_options? table ----@class (exact) oil.PreviewWindowConfig : oil.WindowConfig +---@class (exact) oil.PreviewWindowConfig ---@field update_on_cursor_moved boolean ----@class (exact) oil.SetupPreviewWindowConfig : oil.SetupWindowConfig +---@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig + +---@class (exact) oil.SetupPreviewWindowConfig ---@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved +---@class (exact) oil.SetupConfirmationWindowConfig : oil.SetupWindowConfig + ---@class (exact) oil.ProgressWindowConfig : oil.WindowConfig ---@field minimized_border string|string[] @@ -356,6 +365,7 @@ local M = {} M.setup = function(opts) opts = opts or {} + local new_conf = vim.tbl_deep_extend("keep", opts, default_config) if not new_conf.use_default_keymaps then new_conf.keymaps = opts.keymaps or {} @@ -367,6 +377,11 @@ M.setup = function(opts) end end + -- Backwards compatibility. We renamed the 'preview' window config to be called 'confirmation'. + if opts.preview and not opts.confirmation then + new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation) + end + if new_conf.lsp_rename_autosave ~= nil then new_conf.lsp_file_methods.autosave_changes = new_conf.lsp_rename_autosave new_conf.lsp_rename_autosave = nil diff --git a/lua/oil/layout.lua b/lua/oil/layout.lua index f22d26a..8ed7b4e 100644 --- a/lua/oil/layout.lua +++ b/lua/oil/layout.lua @@ -182,6 +182,11 @@ M.split_window = function(winid, direction, gap) return dim_root, dim_new end +---@param desired_width integer +---@param desired_height integer +---@param opts table +---@return integer width +---@return integer height M.calculate_dims = function(desired_width, desired_height, opts) local width = M.calculate_width(desired_width, opts) local height = M.calculate_height(desired_height, opts) diff --git a/lua/oil/mutator/preview.lua b/lua/oil/mutator/confirmation.lua similarity index 97% rename from lua/oil/mutator/preview.lua rename to lua/oil/mutator/confirmation.lua index 3f8d87d..4e86abc 100644 --- a/lua/oil/mutator/preview.lua +++ b/lua/oil/mutator/confirmation.lua @@ -91,7 +91,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) table.insert(lines, "") -- Create the floating window - local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.preview) + local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation) local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, { relative = "editor", width = width, @@ -100,7 +100,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) col = math.floor((layout.get_editor_width() - width) / 2), zindex = 152, -- render on top of the floating window title style = "minimal", - border = config.preview.border, + border = config.confirmation.border, }) if not ok then vim.notify(string.format("Error showing oil preview window: %s", winid), vim.log.levels.ERROR) @@ -108,7 +108,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) end vim.bo[bufnr].filetype = "oil_preview" vim.bo[bufnr].syntax = "oil_preview" - for k, v in pairs(config.preview.win_options) do + for k, v in pairs(config.confirmation.win_options) do vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) end @@ -155,7 +155,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) vim.api.nvim_create_autocmd("VimResized", { callback = function() if vim.api.nvim_win_is_valid(winid) then - width, height = layout.calculate_dims(max_line_width, #lines, config.preview) + width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation) vim.api.nvim_win_set_config(winid, { relative = "editor", width = width, diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index 8dfd4b0..f15c069 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -3,12 +3,12 @@ local Trie = require("oil.mutator.trie") local cache = require("oil.cache") local columns = require("oil.columns") local config = require("oil.config") +local confirmation = require("oil.mutator.confirmation") local constants = require("oil.constants") local fs = require("oil.fs") local lsp_helpers = require("oil.lsp.helpers") local oil = require("oil") local parser = require("oil.mutator.parser") -local preview = require("oil.mutator.preview") local util = require("oil.util") local view = require("oil.view") local M = {} @@ -564,7 +564,7 @@ M.try_write_changes = function(confirm, cb) end local actions = M.create_actions_from_diffs(all_diffs) - preview.show(actions, confirm, function(proceed) + confirmation.show(actions, confirm, function(proceed) if not proceed then unlock() cb("Canceled") diff --git a/lua/oil/view.lua b/lua/oil/view.lua index 89e4b8f..bf34e8b 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -413,7 +413,7 @@ M.initialize = function(bufnr) timer:again() return end - timer = vim.loop.new_timer() + timer = uv.new_timer() if not timer then return end From eb5497f0ac54f646a14f52002516c540bf5b72fc Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 10 Nov 2024 15:56:15 -0800 Subject: [PATCH 093/206] refactor: rename 'preview' config to 'preview_win' --- README.md | 2 +- doc/oil.txt | 2 +- lua/oil/config.lua | 10 +++++++--- lua/oil/view.lua | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4d91f1d..85b5f01 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ require("oil").setup({ end, }, -- Configuration for the file preview window - preview = { + preview_win = { -- Whether the preview window is automatically updated when the cursor is moved update_on_cursor_moved = true, }, diff --git a/doc/oil.txt b/doc/oil.txt index cafd61e..03b7bc3 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -153,7 +153,7 @@ CONFIG *oil-confi end, }, -- Configuration for the file preview window - preview = { + preview_win = { -- Whether the preview window is automatically updated when the cursor is moved update_on_cursor_moved = true, }, diff --git a/lua/oil/config.lua b/lua/oil/config.lua index bcb7225..31e25f2 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -138,7 +138,7 @@ local default_config = { end, }, -- Configuration for the file preview window - preview = { + preview_win = { -- Whether the preview window is automatically updated when the cursor is moved update_on_cursor_moved = true, }, @@ -221,7 +221,7 @@ default_config.adapter_aliases = {} ---@field extra_scp_args string[] ---@field git oil.GitOptions ---@field float oil.FloatWindowConfig ----@field preview oil.PreviewWindowConfig +---@field preview_win oil.PreviewWindowConfig ---@field confirmation oil.ConfirmationWindowConfig ---@field progress oil.ProgressWindowConfig ---@field ssh oil.SimpleWindowConfig @@ -249,7 +249,7 @@ local M = {} ---@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH ---@field git? oil.SetupGitOptions EXPERIMENTAL support for performing file operations with git ---@field float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float ----@field preview? oil.SetupPreviewWindowConfig Configuration for the file preview window +---@field preview_win? oil.SetupPreviewWindowConfig Configuration for the file preview window ---@field confirmation? oil.SetupConfirmationWindowConfig Configuration for the floating action confirmation window ---@field progress? oil.SetupProgressWindowConfig Configuration for the floating progress window ---@field ssh? oil.SetupSimpleWindowConfig Configuration for the floating SSH window @@ -381,6 +381,10 @@ M.setup = function(opts) if opts.preview and not opts.confirmation then new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation) end + -- Backwards compatibility. We renamed the 'preview' config to 'preview_win' + if opts.preview and opts.preview.update_on_cursor_moved ~= nil then + new_conf.preview_win.update_on_cursor_moved = opts.preview.update_on_cursor_moved + end if new_conf.lsp_rename_autosave ~= nil then new_conf.lsp_file_methods.autosave_changes = new_conf.lsp_rename_autosave diff --git a/lua/oil/view.lua b/lua/oil/view.lua index bf34e8b..da404a3 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -407,7 +407,7 @@ M.initialize = function(bufnr) constrain_cursor() - if config.preview.update_on_cursor_moved then + if config.preview_win.update_on_cursor_moved then -- Debounce and update the preview window if timer then timer:again() From 2f5d4353ee62e117b4e7c3856f16aaf8760867b3 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 10 Nov 2024 16:06:43 -0800 Subject: [PATCH 094/206] doc: improve type annotations for oil.open_preview --- README.md | 2 +- doc/api.md | 19 ++++++++++--------- doc/oil.txt | 12 +++++++----- lua/oil/init.lua | 11 +++++++---- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 85b5f01..6bf0282 100644 --- a/README.md +++ b/README.md @@ -360,7 +360,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i - [toggle_float(dir)](doc/api.md#toggle_floatdir) - [open(dir)](doc/api.md#opendir) - [close()](doc/api.md#close) -- [open_preview(opts)](doc/api.md#open_previewopts) +- [open_preview(opts, callback)](doc/api.md#open_previewopts-callback) - [select(opts, callback)](doc/api.md#selectopts-callback) - [save(opts, cb)](doc/api.md#saveopts-cb) - [setup(opts)](doc/api.md#setupopts) diff --git a/doc/api.md b/doc/api.md index e107293..b1db7cd 100644 --- a/doc/api.md +++ b/doc/api.md @@ -14,7 +14,7 @@ - [toggle_float(dir)](#toggle_floatdir) - [open(dir)](#opendir) - [close()](#close) -- [open_preview(opts)](#open_previewopts) +- [open_preview(opts, callback)](#open_previewopts-callback) - [select(opts, callback)](#selectopts-callback) - [save(opts, cb)](#saveopts-cb) - [setup(opts)](#setupopts) @@ -125,17 +125,18 @@ Open oil browser for a directory Restore the buffer that was present when oil was opened -## open_preview(opts) +## open_preview(opts, callback) -`open_preview(opts)` \ +`open_preview(opts, callback)` \ Preview the entry under the cursor in a split -| Param | Type | Desc | | -| ----- | ------------ | -------------------------------------------------- | ------------------------------------- | -| opts | `nil\|table` | | | -| | vertical | `boolean` | Open the buffer in a vertical split | -| | horizontal | `boolean` | Open the buffer in a horizontal split | -| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| Param | Type | Desc | | +| -------- | ---------------------------- | ------------------------------------------------------- | ------------------------------------- | +| opts | `nil\|oil.OpenPreviewOpts` | | | +| | vertical | `nil\|boolean` | Open the buffer in a vertical split | +| | horizontal | `nil\|boolean` | Open the buffer in a horizontal split | +| | split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| callback | `nil\|fun(err: nil\|string)` | Called once the preview window has been opened | | ## select(opts, callback) diff --git a/doc/oil.txt b/doc/oil.txt index 03b7bc3..b4ebf1f 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -315,15 +315,17 @@ close() *oil.clos Restore the buffer that was present when oil was opened -open_preview({opts}) *oil.open_preview* +open_preview({opts}, {callback}) *oil.open_preview* Preview the entry under the cursor in a split Parameters: - {opts} `nil|table` - {vertical} `boolean` Open the buffer in a vertical split - {horizontal} `boolean` Open the buffer in a horizontal split - {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + {opts} `nil|oil.OpenPreviewOpts` + {vertical} `nil|boolean` Open the buffer in a vertical split + {horizontal} `nil|boolean` Open the buffer in a horizontal split + {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` Split modifier + {callback} `nil|fun(err: nil|string)` Called once the preview window has + been opened select({opts}, {callback}) *oil.select* Select the entry under the cursor diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 8c04d8b..600ccc5 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -410,11 +410,14 @@ M.close = function() vim.api.nvim_buf_delete(oilbuf, { force = true }) end +---@class oil.OpenPreviewOpts +---@field vertical? boolean Open the buffer in a vertical split +---@field horizontal? boolean Open the buffer in a horizontal split +---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier + ---Preview the entry under the cursor in a split ----@param opts nil|table ---- vertical boolean Open the buffer in a vertical split ---- horizontal boolean Open the buffer in a horizontal split ---- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier +---@param opts? oil.OpenPreviewOpts +---@param callback? fun(err: nil|string) Called once the preview window has been opened M.open_preview = function(opts, callback) opts = opts or {} local config = require("oil.config") From 3499e26ef4784a3e09902518a65c21f55fce018a Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Sun, 10 Nov 2024 16:38:45 -0800 Subject: [PATCH 095/206] chore: rework Makefile to not depend on direnv --- .envrc | 1 + .gitignore | 1 + Makefile | 34 +++++++++++++++++++++++++++------- scripts/requirements.txt | 4 ++++ 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 scripts/requirements.txt diff --git a/.envrc b/.envrc index 175de89..32465e7 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ +export VIRTUAL_ENV=venv layout python diff --git a/.gitignore b/.gitignore index 290f656..c90db5d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ __pycache__ .direnv/ .testenv/ +venv/ doc/tags scripts/nvim_doc_tools scripts/nvim-typecheck-action diff --git a/Makefile b/Makefile index 71447f8..4799368 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,37 @@ -.PHONY: all doc test lint fastlint clean +## help: print this help message +.PHONY: help +help: + @echo 'Usage:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' +## all: generate docs, lint, and run tests +.PHONY: all all: doc lint test -doc: scripts/nvim_doc_tools - python scripts/main.py generate - python scripts/main.py lint +venv: + python3 -m venv venv + venv/bin/pip install -r scripts/requirements.txt +## doc: generate documentation +.PHONY: doc +doc: scripts/nvim_doc_tools venv + venv/bin/python scripts/main.py generate + venv/bin/python scripts/main.py lint + +## test: run tests +.PHONY: test test: ./run_tests.sh +## lint: run linters and LuaLS typechecking +.PHONY: lint lint: scripts/nvim-typecheck-action fastlint ./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua -fastlint: scripts/nvim_doc_tools - python scripts/main.py lint +## fastlint: run only fast linters +.PHONY: fastlint +fastlint: scripts/nvim_doc_tools venv + venv/bin/python scripts/main.py lint luacheck lua tests --formatter plain stylua --check lua tests @@ -23,5 +41,7 @@ scripts/nvim_doc_tools: scripts/nvim-typecheck-action: git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action +## clean: reset the repository to a clean state +.PHONY: clean clean: - rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action + rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..2c6271f --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,4 @@ +pyparsing==3.0.9 +black +isort +mypy From 6e754e66997a1739a7cb0476195c8522fd72fe3a Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 11 Nov 2024 00:40:33 +0000 Subject: [PATCH 096/206] [docgen] Update docs skip-checks: true --- doc/api.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/api.md b/doc/api.md index b1db7cd..35c6392 100644 --- a/doc/api.md +++ b/doc/api.md @@ -130,39 +130,39 @@ Restore the buffer that was present when oil was opened `open_preview(opts, callback)` \ Preview the entry under the cursor in a split -| Param | Type | Desc | | -| -------- | ---------------------------- | ------------------------------------------------------- | ------------------------------------- | -| opts | `nil\|oil.OpenPreviewOpts` | | | -| | vertical | `nil\|boolean` | Open the buffer in a vertical split | -| | horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| | split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| callback | `nil\|fun(err: nil\|string)` | Called once the preview window has been opened | | +| Param | Type | Desc | +| ----------- | ------------------------------------------------------- | ---------------------------------------------- | +| opts | `nil\|oil.OpenPreviewOpts` | | +| >vertical | `nil\|boolean` | Open the buffer in a vertical split | +| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | +| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| callback | `nil\|fun(err: nil\|string)` | Called once the preview window has been opened | ## select(opts, callback) `select(opts, callback)` \ Select the entry under the cursor -| Param | Type | Desc | | -| -------- | ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------- | -| opts | `nil\|oil.SelectOpts` | | | -| | vertical | `nil\|boolean` | Open the buffer in a vertical split | -| | horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| | split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| | tab | `nil\|boolean` | Open the buffer in a new tab | -| | close | `nil\|boolean` | Close the original oil buffer once selection is made | -| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | | +| Param | Type | Desc | +| ----------- | ------------------------------------------------------- | ---------------------------------------------------- | +| opts | `nil\|oil.SelectOpts` | | +| >vertical | `nil\|boolean` | Open the buffer in a vertical split | +| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | +| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| >tab | `nil\|boolean` | Open the buffer in a new tab | +| >close | `nil\|boolean` | Close the original oil buffer once selection is made | +| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | ## save(opts, cb) `save(opts, cb)` \ Save all changes -| Param | Type | Desc | | -| ----- | ---------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------- | -| opts | `nil\|table` | | | -| | confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | -| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. | | +| Param | Type | Desc | +| -------- | ---------------------------- | ------------------------------------------------------------------------------------------- | +| opts | `nil\|table` | | +| >confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | +| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. | **Note:**

From 50c4bd4ee216f08907f64d0295c0663a69e58ffb Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sun, 10 Nov 2024 19:18:46 -0800
Subject: [PATCH 097/206] chore(master): release 2.13.0 (#478)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
 CHANGELOG.md | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39811f8..3e7284a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
 # Changelog
 
+## [2.13.0](https://github.com/stevearc/oil.nvim/compare/v2.12.2...v2.13.0) (2024-11-11)
+
+
+### Features
+
+* config option to customize floating window title ([#482](https://github.com/stevearc/oil.nvim/issues/482)) ([5d2dfae](https://github.com/stevearc/oil.nvim/commit/5d2dfae655b9b689bd4017b3bdccd52cbee5b92f))
+* config option to disable lsp file methods ([#477](https://github.com/stevearc/oil.nvim/issues/477)) ([f60bb7f](https://github.com/stevearc/oil.nvim/commit/f60bb7f793477d99ef1acf39e920bf2ca4e644de))
+
+
+### Bug Fixes
+
+* actions.preview accepts options ([#497](https://github.com/stevearc/oil.nvim/issues/497)) ([cca1631](https://github.com/stevearc/oil.nvim/commit/cca1631d5ea450c09ba72f3951a9e28105a3632c))
+* add trailing slash to directories on yank_entry ([#504](https://github.com/stevearc/oil.nvim/issues/504)) ([42333bb](https://github.com/stevearc/oil.nvim/commit/42333bb46e34dd47e13927010b1dcd30e6e4ca96))
+* don't deep merge keymaps ([#510](https://github.com/stevearc/oil.nvim/issues/510)) ([709403c](https://github.com/stevearc/oil.nvim/commit/709403ccd6f22d859c2e42c780ab558ae89284d9))
+* guard against nil keymaps ([621f8ba](https://github.com/stevearc/oil.nvim/commit/621f8ba4fa821724e9b646732a26fb2e795fe008))
+* only map ~ for normal mode ([#484](https://github.com/stevearc/oil.nvim/issues/484)) ([ccab9d5](https://github.com/stevearc/oil.nvim/commit/ccab9d5e09e2d0042fbbe5b6bd05e82426247067))
+* sort keymap help entries by description ([#506](https://github.com/stevearc/oil.nvim/issues/506)) ([52cc8a1](https://github.com/stevearc/oil.nvim/commit/52cc8a1fb35ea6ce1df536143add7ce7215c63c0)), closes [#376](https://github.com/stevearc/oil.nvim/issues/376)
+
 ## [2.12.2](https://github.com/stevearc/oil.nvim/compare/v2.12.1...v2.12.2) (2024-09-10)
 
 

From c23fe08e0546d9efc242e19f0d829efa7e7b2743 Mon Sep 17 00:00:00 2001
From: Steve Walker <65963536+stevalkr@users.noreply.github.com>
Date: Wed, 13 Nov 2024 00:24:39 +0800
Subject: [PATCH 098/206] feat: disable preview for large files (#511)

* feat: disable preview for large files

fix: update oil.PreviewWindowConfig

* refactor: remove unnecessary shim in config.lua

* refactor: revert changes to shim

---------

Co-authored-by: Steve Walker <65963536+etherswangel@users.noreply.github.com>
Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 README.md          | 2 ++
 doc/oil.txt        | 2 ++
 lua/oil/config.lua | 4 ++++
 lua/oil/init.lua   | 7 +++++++
 4 files changed, 15 insertions(+)

diff --git a/README.md b/README.md
index 6bf0282..79b9636 100644
--- a/README.md
+++ b/README.md
@@ -271,6 +271,8 @@ require("oil").setup({
   preview_win = {
     -- Whether the preview window is automatically updated when the cursor is moved
     update_on_cursor_moved = true,
+    -- Maximum file size in megabytes to preview
+    max_file_size_mb = 100,
   },
   -- Configuration for the floating action confirmation window
   confirmation = {
diff --git a/doc/oil.txt b/doc/oil.txt
index b4ebf1f..3283036 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -156,6 +156,8 @@ CONFIG                                                                *oil-confi
       preview_win = {
         -- Whether the preview window is automatically updated when the cursor is moved
         update_on_cursor_moved = true,
+        -- Maximum file size in megabytes to preview
+        max_file_size_mb = 100,
       },
       -- Configuration for the floating action confirmation window
       confirmation = {
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 31e25f2..17874b7 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -141,6 +141,8 @@ local default_config = {
   preview_win = {
     -- Whether the preview window is automatically updated when the cursor is moved
     update_on_cursor_moved = true,
+    -- Maximum file size in megabytes to preview
+    max_file_size_mb = 100,
   },
   -- Configuration for the floating action confirmation window
   confirmation = {
@@ -323,11 +325,13 @@ local M = {}
 
 ---@class (exact) oil.PreviewWindowConfig
 ---@field update_on_cursor_moved boolean
+---@field max_file_size_mb number
 
 ---@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig
 
 ---@class (exact) oil.SetupPreviewWindowConfig
 ---@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved
+---@field max_file_size_mb? number Maximum file size in megabytes to preview
 
 ---@class (exact) oil.SetupConfirmationWindowConfig : oil.SetupWindowConfig
 
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 600ccc5..69f9780 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -452,6 +452,13 @@ M.open_preview = function(opts, callback)
   if not entry then
     return finish("Could not find entry under cursor")
   end
+  if entry.meta ~= nil and entry.meta.stat ~= nil then
+    if entry.meta.stat.size >= config.preview_win.max_file_size_mb * 1e6 then
+      return finish(
+        "File over " .. config.preview_win.max_file_size_mb .. "MB is too large to preview"
+      )
+    end
+  end
   local entry_title = entry.name
   if entry.type == "directory" then
     entry_title = entry_title .. "/"

From bbeed86bde134da8d09bed64b6aa0d65642e6b23 Mon Sep 17 00:00:00 2001
From: Micah Halter 
Date: Tue, 12 Nov 2024 13:38:35 -0500
Subject: [PATCH 099/206] feat: add `win_options` to `preview_win` (#514)

---
 lua/oil/config.lua | 4 ++++
 lua/oil/init.lua   | 3 +++
 lua/oil/view.lua   | 5 +++++
 3 files changed, 12 insertions(+)

diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 17874b7..f203e87 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -143,6 +143,8 @@ local default_config = {
     update_on_cursor_moved = true,
     -- Maximum file size in megabytes to preview
     max_file_size_mb = 100,
+    -- Window-local options to use for preview window buffers
+    win_options = {},
   },
   -- Configuration for the floating action confirmation window
   confirmation = {
@@ -326,12 +328,14 @@ local M = {}
 ---@class (exact) oil.PreviewWindowConfig
 ---@field update_on_cursor_moved boolean
 ---@field max_file_size_mb number
+---@field win_options table
 
 ---@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig
 
 ---@class (exact) oil.SetupPreviewWindowConfig
 ---@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved
 ---@field max_file_size_mb? number Maximum file size in megabytes to preview
+---@field win_options? table Window-local options to use for preview window buffers
 
 ---@class (exact) oil.SetupConfirmationWindowConfig : oil.SetupWindowConfig
 
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 69f9780..2399d39 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -551,6 +551,9 @@ M.open_preview = function(opts, callback)
     end
 
     vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 })
+    for k, v in pairs(config.preview_win.win_options) do
+      vim.api.nvim_set_option_value(k, v, { scope = "local", win = preview_win })
+    end
     vim.w.oil_entry_id = entry.id
     vim.w.oil_source_win = prev_win
     if is_visual_mode then
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index da404a3..ddc5db3 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -182,6 +182,11 @@ M.set_win_options = function()
   for k, v in pairs(config.win_options) do
     vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
   end
+  if vim.wo[winid].previewwindow then -- apply preview window options last
+    for k, v in pairs(config.preview_win.win_options) do
+      vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
+    end
+  end
 end
 
 ---Get a list of visible oil buffers and a list of hidden oil buffers

From 8735d185b37457bd899cd4e47a4517b899407949 Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Tue, 12 Nov 2024 18:38:53 +0000
Subject: [PATCH 100/206] [docgen] Update docs skip-checks: true

---
 README.md   | 2 ++
 doc/oil.txt | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/README.md b/README.md
index 79b9636..b6b970e 100644
--- a/README.md
+++ b/README.md
@@ -273,6 +273,8 @@ require("oil").setup({
     update_on_cursor_moved = true,
     -- Maximum file size in megabytes to preview
     max_file_size_mb = 100,
+    -- Window-local options to use for preview window buffers
+    win_options = {},
   },
   -- Configuration for the floating action confirmation window
   confirmation = {
diff --git a/doc/oil.txt b/doc/oil.txt
index 3283036..44bdcfa 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -158,6 +158,8 @@ CONFIG                                                                *oil-confi
         update_on_cursor_moved = true,
         -- Maximum file size in megabytes to preview
         max_file_size_mb = 100,
+        -- Window-local options to use for preview window buffers
+        win_options = {},
       },
       -- Configuration for the floating action confirmation window
       confirmation = {

From 0472d9296ace2d57769eb3e022a918803f096ea4 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 13 Nov 2024 08:58:16 -0800
Subject: [PATCH 101/206] lint: fix typechecking for new LuaLS version

---
 lua/oil/adapters/files/permissions.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/oil/adapters/files/permissions.lua b/lua/oil/adapters/files/permissions.lua
index cf50b55..6c306a6 100644
--- a/lua/oil/adapters/files/permissions.lua
+++ b/lua/oil/adapters/files/permissions.lua
@@ -1,6 +1,6 @@
 local M = {}
 
----@param exe_modifier nil|false|string
+---@param exe_modifier false|string
 ---@param num integer
 ---@return string
 local function perm_to_str(exe_modifier, num)

From 7d4e62942f647796d24b4ae22b84a75c41750fb7 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 14 Nov 2024 19:18:18 -0800
Subject: [PATCH 102/206] test: add harness for measuring performance

---
 .envrc                 |   1 +
 .gitignore             |   2 +
 Makefile               |  19 ++++++-
 tests/perf_harness.lua | 122 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 143 insertions(+), 1 deletion(-)
 create mode 100644 tests/perf_harness.lua

diff --git a/.envrc b/.envrc
index 32465e7..d522e34 100644
--- a/.envrc
+++ b/.envrc
@@ -1,2 +1,3 @@
 export VIRTUAL_ENV=venv
 layout python
+python -c 'import pyparsing' 2>/dev/null || pip install -r scripts/requirements.txt
diff --git a/.gitignore b/.gitignore
index c90db5d..bb036c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,3 +48,5 @@ venv/
 doc/tags
 scripts/nvim_doc_tools
 scripts/nvim-typecheck-action
+tests/perf/
+profile.json
diff --git a/Makefile b/Makefile
index 4799368..bd7bf6c 100644
--- a/Makefile
+++ b/Makefile
@@ -35,6 +35,23 @@ fastlint: scripts/nvim_doc_tools venv
 	luacheck lua tests --formatter plain
 	stylua --check lua tests
 
+## profile: use LuaJIT profiler to profile the plugin
+.PHONY: profile
+profile:
+	nvim --clean -u tests/perf_harness.lua -c 'lua jit_profile()'
+
+## flame_profile: create a trace in the chrome profiler format
+.PHONY: flame_profile
+flame_profile:
+	nvim --clean -u tests/perf_harness.lua -c 'lua flame_profile()'
+	@echo "Visit https://ui.perfetto.dev/ and load the profile.json file"
+
+## benchmark: benchmark performance opening directory with many files
+.PHONY: benchmark
+benchmark:
+	nvim --clean -u tests/perf_harness.lua -c 'lua benchmark(10)'
+	@cat tests/perf/benchmark.txt
+
 scripts/nvim_doc_tools:
 	git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools
 
@@ -44,4 +61,4 @@ scripts/nvim-typecheck-action:
 ## clean: reset the repository to a clean state
 .PHONY: clean
 clean:
-	rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv
+	rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv tests/perf profile.json
diff --git a/tests/perf_harness.lua b/tests/perf_harness.lua
new file mode 100644
index 0000000..679004d
--- /dev/null
+++ b/tests/perf_harness.lua
@@ -0,0 +1,122 @@
+vim.fn.mkdir("tests/perf/.env", "p")
+local root = vim.fn.fnamemodify("./tests/perf/.env", ":p")
+
+for _, name in ipairs({ "config", "data", "state", "runtime", "cache" }) do
+  vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. name
+end
+
+vim.opt.runtimepath:prepend(vim.fn.fnamemodify(".", ":p"))
+
+---@module 'oil'
+---@type oil.SetupOpts
+local setup_opts = {
+  -- columns = { "icon", "permissions", "size", "mtime" },
+}
+
+local num_files = 100000
+
+if not vim.uv.fs_stat(string.format("tests/perf/file %d.txt", num_files)) then
+  vim.notify("Creating files")
+  for i = 1, num_files, 1 do
+    local filename = ("tests/perf/file %d.txt"):format(i)
+    local fd = vim.uv.fs_open(filename, "a", 420)
+    assert(fd)
+    vim.uv.fs_close(fd)
+  end
+end
+
+local function wait_for_done(callback)
+  vim.api.nvim_create_autocmd("User", {
+    pattern = "OilEnter",
+    once = true,
+    callback = callback,
+  })
+end
+
+function _G.jit_profile()
+  require("oil").setup(setup_opts)
+  local outfile = "tests/perf/profile.txt"
+  require("jit.p").start("3Fpli1s", outfile)
+  local start = vim.uv.hrtime()
+  require("oil").open("tests/perf")
+
+  wait_for_done(function()
+    local delta = vim.uv.hrtime() - start
+    require("jit.p").stop()
+    print("Elapsed:", delta / 1e6, "ms")
+    vim.cmd.edit({ args = { outfile } })
+  end)
+end
+
+function _G.benchmark(iterations)
+  require("oil").setup(setup_opts)
+  local num_outliers = math.floor(0.1 * iterations)
+  local times = {}
+
+  local run_profile
+  run_profile = function()
+    -- Clear out state
+    vim.cmd.enew()
+    for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
+      if vim.api.nvim_buf_is_valid(bufnr) and bufnr ~= vim.api.nvim_get_current_buf() then
+        vim.api.nvim_buf_delete(bufnr, { force = true })
+      end
+    end
+
+    local start = vim.uv.hrtime()
+    wait_for_done(function()
+      local delta = vim.uv.hrtime() - start
+      table.insert(times, delta / 1e6)
+      if #times < iterations then
+        vim.schedule(run_profile)
+      else
+        -- Remove the outliers
+        table.sort(times)
+        for _ = 1, num_outliers do
+          table.remove(times, 1)
+          table.remove(times)
+        end
+
+        local total = 0
+        for _, time in ipairs(times) do
+          total = total + time
+        end
+
+        local lines = {
+          table.concat(
+            vim.tbl_map(function(t)
+              return string.format("%dms", math.floor(t))
+            end, times),
+            " "
+          ),
+          string.format("Average: %dms", math.floor(total / #times)),
+        }
+        vim.fn.writefile(lines, "tests/perf/benchmark.txt")
+        vim.cmd.qall()
+      end
+    end)
+    require("oil").open("tests/perf")
+  end
+
+  run_profile()
+end
+
+function _G.flame_profile()
+  if not vim.uv.fs_stat("tests/perf/profile.nvim") then
+    vim
+      .system({ "git", "clone", "https://github.com/stevearc/profile.nvim", "tests/perf/profile.nvim" })
+      :wait()
+  end
+  vim.opt.runtimepath:prepend(vim.fn.fnamemodify("./tests/perf/profile.nvim", ":p"))
+  local profile = require("profile")
+  profile.instrument_autocmds()
+  profile.instrument("oil*")
+
+  require("oil").setup(setup_opts)
+  profile.start()
+  require("oil").open("tests/perf")
+  wait_for_done(function()
+    profile.stop("profile.json")
+    vim.cmd.qall()
+  end)
+end

From 01b0b9d8ef79b7b631e92f6b5fed1c639262d570 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 14 Nov 2024 19:18:19 -0800
Subject: [PATCH 103/206] perf: change default view_options.natural_order
 behavior to disable on large directories

---
 README.md           |  6 +++---
 doc/oil.txt         |  6 +++---
 lua/oil/columns.lua | 36 +++++++++++++++++++++++++-----------
 lua/oil/config.lua  | 10 +++++-----
 lua/oil/view.lua    |  9 ++++++---
 5 files changed, 42 insertions(+), 25 deletions(-)

diff --git a/README.md b/README.md
index b6b970e..8c0bdb3 100644
--- a/README.md
+++ b/README.md
@@ -220,9 +220,9 @@ require("oil").setup({
     is_always_hidden = function(name, bufnr)
       return false
     end,
-    -- Sort file names in a more intuitive order for humans. Is less performant,
-    -- so you may want to set to false if you work with large directories.
-    natural_order = true,
+    -- Sort file names with numbers in a more intuitive order for humans.
+    -- Can be "fast", true, or false. "fast" will turn it off for large directories.
+    natural_order = "fast",
     -- Sort file and directory names case insensitive
     case_insensitive = false,
     sort = {
diff --git a/doc/oil.txt b/doc/oil.txt
index 44bdcfa..b655206 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -105,9 +105,9 @@ CONFIG                                                                *oil-confi
         is_always_hidden = function(name, bufnr)
           return false
         end,
-        -- Sort file names in a more intuitive order for humans. Is less performant,
-        -- so you may want to set to false if you work with large directories.
-        natural_order = true,
+        -- Sort file names with numbers in a more intuitive order for humans.
+        -- Can be "fast", true, or false. "fast" will turn it off for large directories.
+        natural_order = "fast",
         -- Sort file and directory names case insensitive
         case_insensitive = false,
         sort = {
diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua
index 40b7d74..d882792 100644
--- a/lua/oil/columns.lua
+++ b/lua/oil/columns.lua
@@ -19,6 +19,7 @@ local all_columns = {}
 ---@field render_action? fun(action: oil.ChangeAction): string
 ---@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string))
 ---@field get_sort_value? fun(entry: oil.InternalEntry): number|string
+---@field create_sort_value_factory? fun(num_entries: integer): fun(entry: oil.InternalEntry): number|string
 
 ---@param name string
 ---@param column oil.ColumnDefinition
@@ -292,18 +293,31 @@ M.register("name", {
     error("Do not use the name column. It is for sorting only")
   end,
 
-  get_sort_value = function(entry)
-    local sort_value = entry[FIELD_NAME]
-
-    if config.view_options.natural_order then
-      sort_value = sort_value:gsub("%d+", pad_number)
+  create_sort_value_factory = function(num_entries)
+    if
+      config.view_options.natural_order == false
+      or (config.view_options.natural_order == "fast" and num_entries > 5000)
+    then
+      if config.view_options.case_insensitive then
+        return function(entry)
+          return entry[FIELD_NAME]:lower()
+        end
+      else
+        return function(entry)
+          return entry[FIELD_NAME]
+        end
+      end
+    else
+      if config.view_options.case_insensitive then
+        return function(entry)
+          return entry[FIELD_NAME]:gsub("%d+", pad_number):lower()
+        end
+      else
+        return function(entry)
+          return entry[FIELD_NAME]:gsub("%d+", pad_number)
+        end
+      end
     end
-
-    if config.view_options.case_insensitive then
-      sort_value = sort_value:lower()
-    end
-
-    return sort_value
   end,
 })
 
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index f203e87..5ced37d 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -90,9 +90,9 @@ local default_config = {
     is_always_hidden = function(name, bufnr)
       return false
     end,
-    -- Sort file names in a more intuitive order for humans. Is less performant,
-    -- so you may want to set to false if you work with large directories.
-    natural_order = true,
+    -- Sort file names with numbers in a more intuitive order for humans.
+    -- Can be "fast", true, or false. "fast" will turn it off for large directories.
+    natural_order = "fast",
     -- Sort file and directory names case insensitive
     case_insensitive = false,
     sort = {
@@ -273,7 +273,7 @@ local M = {}
 ---@field show_hidden boolean
 ---@field is_hidden_file fun(name: string, bufnr: integer): boolean
 ---@field is_always_hidden fun(name: string, bufnr: integer): boolean
----@field natural_order boolean
+---@field natural_order boolean|"fast"
 ---@field case_insensitive boolean
 ---@field sort oil.SortSpec[]
 
@@ -281,7 +281,7 @@ local M = {}
 ---@field show_hidden? boolean Show files and directories that start with "."
 ---@field is_hidden_file? fun(name: string, bufnr: integer): boolean This function defines what is considered a "hidden" file
 ---@field is_always_hidden? fun(name: string, bufnr: integer): boolean This function defines what will never be shown, even when `show_hidden` is set
----@field natural_order? boolean Sort file names in a more intuitive order for humans. Is less performant, so you may want to set to false if you work with large directories.
+---@field natural_order? boolean|"fast" Sort file names with numbers in a more intuitive order for humans. Can be slow for large directories.
 ---@field case_insensitive? boolean Sort file and directory names case insensitive
 ---@field sort? oil.SortSpec[] Sort order for the file list
 
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index ddc5db3..8aa8ed7 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -537,8 +537,9 @@ M.initialize = function(bufnr)
 end
 
 ---@param adapter oil.Adapter
+---@param num_entries integer
 ---@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean
-local function get_sort_function(adapter)
+local function get_sort_function(adapter, num_entries)
   local idx_funs = {}
   local sort_config = config.view_options.sort
 
@@ -560,7 +561,9 @@ local function get_sort_function(adapter)
       )
     end
     local col = columns.get_column(adapter, col_name)
-    if col and col.get_sort_value then
+    if col and col.create_sort_value_factory then
+      table.insert(idx_funs, { col.create_sort_value_factory(num_entries), order })
+    elseif col and col.get_sort_value then
       table.insert(idx_funs, { col.get_sort_value, order })
     else
       vim.notify_once(
@@ -611,7 +614,7 @@ local function render_buffer(bufnr, opts)
   local entries = cache.list_url(bufname)
   local entry_list = vim.tbl_values(entries)
 
-  table.sort(entry_list, get_sort_function(adapter))
+  table.sort(entry_list, get_sort_function(adapter, #entry_list))
 
   local jump_idx
   if opts.jump_first then

From 4de30256c32cd272482bc6df0c6de78ffc389153 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 14 Nov 2024 19:18:20 -0800
Subject: [PATCH 104/206] perf: replace vim.endswith and vim.startswith with
 string.match

---
 README.md          | 3 ++-
 doc/oil.txt        | 3 ++-
 lua/oil/config.lua | 3 ++-
 lua/oil/util.lua   | 3 ++-
 4 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 8c0bdb3..a3f3013 100644
--- a/README.md
+++ b/README.md
@@ -214,7 +214,8 @@ require("oil").setup({
     show_hidden = false,
     -- This function defines what is considered a "hidden" file
     is_hidden_file = function(name, bufnr)
-      return vim.startswith(name, ".")
+      local m = name:match("^%.")
+      return m ~= nil
     end,
     -- This function defines what will never be shown, even when `show_hidden` is set
     is_always_hidden = function(name, bufnr)
diff --git a/doc/oil.txt b/doc/oil.txt
index b655206..58dee6a 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -99,7 +99,8 @@ CONFIG                                                                *oil-confi
         show_hidden = false,
         -- This function defines what is considered a "hidden" file
         is_hidden_file = function(name, bufnr)
-          return vim.startswith(name, ".")
+          local m = name:match("^%.")
+          return m ~= nil
         end,
         -- This function defines what will never be shown, even when `show_hidden` is set
         is_always_hidden = function(name, bufnr)
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 5ced37d..d3eca47 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -84,7 +84,8 @@ local default_config = {
     show_hidden = false,
     -- This function defines what is considered a "hidden" file
     is_hidden_file = function(name, bufnr)
-      return vim.startswith(name, ".")
+      local m = name:match("^%.")
+      return m ~= nil
     end,
     -- This function defines what will never be shown, even when `show_hidden` is set
     is_always_hidden = function(name, bufnr)
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index f5c63e2..947b38a 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -347,7 +347,8 @@ M.addslash = function(path, os_slash)
     slash = "\\"
   end
 
-  if not vim.endswith(path, slash) then
+  local endslash = path:match(slash .. "$")
+  if not endslash then
     return path .. slash
   else
     return path

From 792f0db6ba8b626b14bc127e1ce7247185b3be91 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 14 Nov 2024 19:18:21 -0800
Subject: [PATCH 105/206] perf: only sort entries after we have them all

---
 lua/oil/view.lua | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 8aa8ed7..cd39a52 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -614,7 +614,10 @@ local function render_buffer(bufnr, opts)
   local entries = cache.list_url(bufname)
   local entry_list = vim.tbl_values(entries)
 
-  table.sort(entry_list, get_sort_function(adapter, #entry_list))
+  -- Only sort the entries once we have them all
+  if not vim.b[bufnr].oil_rendering then
+    table.sort(entry_list, get_sort_function(adapter, #entry_list))
+  end
 
   local jump_idx
   if opts.jump_first then

From c96f93d894cc97e76b0871bec4058530eee8ece4 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 14 Nov 2024 19:18:21 -0800
Subject: [PATCH 106/206] perf: optimize rendering cadence

---
 lua/oil/view.lua | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index cd39a52..3200c49 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -849,6 +849,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
   end
 
   cache.begin_update_url(bufname)
+  local num_iterations = 0
   adapter.list(bufname, get_used_columns(), function(err, entries, fetch_more)
     loading.set_loading(bufnr, false)
     if err then
@@ -865,11 +866,13 @@ M.render_buffer_async = function(bufnr, opts, callback)
       local now = uv.hrtime() / 1e6
       local delta = now - start_ms
       -- If we've been chugging for more than 40ms, go ahead and render what we have
-      if delta > 40 then
+      if (delta > 25 and num_iterations < 1) or delta > 500 then
+        num_iterations = num_iterations + 1
         start_ms = now
         vim.schedule(function()
           seek_after_render_found =
             render_buffer(bufnr, { jump = not seek_after_render_found, jump_first = first })
+          start_ms = uv.hrtime() / 1e6
         end)
       end
       first = false

From 651299a6ca799f09997956f30c67329c6033dcd3 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 14 Nov 2024 19:47:50 -0800
Subject: [PATCH 107/206] doc: trashing on windows works now

---
 doc/oil.txt         | 2 +-
 scripts/generate.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index 58dee6a..5bb0953 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -667,7 +667,7 @@ Mac:
     (instead of being able to see files that were trashed from a directory).
 
 Windows:
-    Oil does not yet support the Windows trash. PRs are welcome!
+    Oil supports the Windows Recycle Bin. All features should work.
 
 ================================================================================
 vim:tw=80:ts=2:ft=help:norl:syntax=help:
diff --git a/scripts/generate.py b/scripts/generate.py
index 5cc5fe3..a20ad53 100755
--- a/scripts/generate.py
+++ b/scripts/generate.py
@@ -366,7 +366,7 @@ Mac:
     (instead of being able to see files that were trashed from a directory).
 
 Windows:
-    Oil does not yet support the Windows trash. PRs are welcome!
+    Oil supports the Windows Recycle Bin. All features should work.
 """
     )
     return section

From 8ea40b5506115b6d355e304dd9ee5089f7d78601 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 14 Nov 2024 22:21:11 -0800
Subject: [PATCH 108/206] fix: cursor sometimes does not hover previous file

---
 lua/oil/view.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 3200c49..b36140b 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -646,7 +646,6 @@ local function render_buffer(bufnr, opts)
       if seek_after_render == name then
         seek_after_render_found = true
         jump_idx = #line_table
-        M.set_last_cursor(bufname, nil)
       end
     end
   end
@@ -825,6 +824,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
     vim.b[bufnr].oil_rendering = false
     loading.set_loading(bufnr, false)
     render_buffer(bufnr, { jump = true })
+    M.set_last_cursor(bufname, nil)
     vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" })
     vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr)
     if callback then

From 21705a1debe6d85a53c138ab944484b685432b2b Mon Sep 17 00:00:00 2001
From: Jalal El Mansouri 
Date: Wed, 20 Nov 2024 02:24:24 +0100
Subject: [PATCH 109/206] feat: use scratch buffer for file previews (#467)

* Initial implementation of scratch based preview

* Fix call to buf is valid in loop

* Fixing call to be made only from the main event loop

* Improve handling of large files from @pkazmier

* Better error handling and simplifying the code

* Default to old behavior

* Add documentation

* Fix readfile

* Fix the configuration

* refactor: single config enum and load real buffer on BufEnter

* doc: regenerate documentation

---------

Co-authored-by: Steven Arcangeli 
---
 README.md          |  4 ++--
 doc/oil.txt        |  4 ++--
 lua/oil/config.lua | 13 +++++++----
 lua/oil/init.lua   | 24 ++++++++++----------
 lua/oil/util.lua   | 55 ++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 79 insertions(+), 21 deletions(-)

diff --git a/README.md b/README.md
index a3f3013..0f4f3fe 100644
--- a/README.md
+++ b/README.md
@@ -272,8 +272,8 @@ require("oil").setup({
   preview_win = {
     -- Whether the preview window is automatically updated when the cursor is moved
     update_on_cursor_moved = true,
-    -- Maximum file size in megabytes to preview
-    max_file_size_mb = 100,
+    -- How to open the preview window "load"|"scratch"|"fast_scratch"
+    preview_method = "fast_scratch",
     -- Window-local options to use for preview window buffers
     win_options = {},
   },
diff --git a/doc/oil.txt b/doc/oil.txt
index 5bb0953..48df3a5 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -157,8 +157,8 @@ CONFIG                                                                *oil-confi
       preview_win = {
         -- Whether the preview window is automatically updated when the cursor is moved
         update_on_cursor_moved = true,
-        -- Maximum file size in megabytes to preview
-        max_file_size_mb = 100,
+        -- How to open the preview window "load"|"scratch"|"fast_scratch"
+        preview_method = "fast_scratch",
         -- Window-local options to use for preview window buffers
         win_options = {},
       },
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index d3eca47..3b6b57e 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -142,8 +142,8 @@ local default_config = {
   preview_win = {
     -- Whether the preview window is automatically updated when the cursor is moved
     update_on_cursor_moved = true,
-    -- Maximum file size in megabytes to preview
-    max_file_size_mb = 100,
+    -- How to open the preview window "load"|"scratch"|"fast_scratch"
+    preview_method = "fast_scratch",
     -- Window-local options to use for preview window buffers
     win_options = {},
   },
@@ -326,16 +326,21 @@ local M = {}
 ---@field border? string|string[] Window border
 ---@field win_options? table
 
+---@alias oil.PreviewMethod
+---| '"load"' # Load the previewed file into a buffer
+---| '"scratch"' # Put the text into a scratch buffer to avoid LSP attaching
+---| '"fast_scratch"' # Put only the visible text into a scratch buffer
+
 ---@class (exact) oil.PreviewWindowConfig
 ---@field update_on_cursor_moved boolean
----@field max_file_size_mb number
+---@field preview_method oil.PreviewMethod
 ---@field win_options table
 
 ---@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig
 
 ---@class (exact) oil.SetupPreviewWindowConfig
 ---@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved
----@field max_file_size_mb? number Maximum file size in megabytes to preview
+---@field preview_method? oil.PreviewMethod How to open the preview window
 ---@field win_options? table Window-local options to use for preview window buffers
 
 ---@class (exact) oil.SetupConfirmationWindowConfig : oil.SetupWindowConfig
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 2399d39..d673484 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -452,13 +452,6 @@ M.open_preview = function(opts, callback)
   if not entry then
     return finish("Could not find entry under cursor")
   end
-  if entry.meta ~= nil and entry.meta.stat ~= nil then
-    if entry.meta.stat.size >= config.preview_win.max_file_size_mb * 1e6 then
-      return finish(
-        "File over " .. config.preview_win.max_file_size_mb .. "MB is too large to preview"
-      )
-    end
-  end
   local entry_title = entry.name
   if entry.type == "directory" then
     entry_title = entry_title .. "/"
@@ -529,14 +522,19 @@ M.open_preview = function(opts, callback)
       end
     end
 
-    local filebufnr = vim.fn.bufadd(normalized_url)
     local entry_is_file = not vim.endswith(normalized_url, "/")
+    local filebufnr
+    if entry_is_file and config.preview_win.preview_method ~= "load" then
+      filebufnr =
+        util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method)
+    end
 
-    -- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after
-    -- we close the window
-    if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then
-      vim.bo[filebufnr].bufhidden = "wipe"
-      vim.b[filebufnr].oil_preview_buffer = true
+    if not filebufnr then
+      filebufnr = vim.fn.bufadd(normalized_url)
+      if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then
+        vim.bo[filebufnr].bufhidden = "wipe"
+        vim.b[filebufnr].oil_preview_buffer = true
+      end
     end
 
     ---@diagnostic disable-next-line: param-type-mismatch
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 947b38a..24b714b 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -897,4 +897,59 @@ M.get_icon_provider = function()
   end
 end
 
+---Read a buffer into a scratch buffer and apply syntactic highlighting when possible
+---@param path string The path to the file to read
+---@param preview_method oil.PreviewMethod
+---@return nil|integer
+M.read_file_to_scratch_buffer = function(path, preview_method)
+  local bufnr = vim.api.nvim_create_buf(false, true)
+  if bufnr == 0 then
+    return
+  end
+
+  vim.bo[bufnr].bufhidden = "wipe"
+  vim.bo[bufnr].buftype = "nofile"
+
+  local max_lines = preview_method == "fast_scratch" and vim.o.lines or nil
+  local has_lines, read_res = pcall(vim.fn.readfile, path, "", max_lines)
+  local lines = has_lines and vim.split(table.concat(read_res, "\n"), "\n") or {}
+
+  local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
+  if not ok then
+    return
+  end
+  local ft = vim.filetype.match({ filename = path, buf = bufnr })
+  if ft and ft ~= "" then
+    local lang = vim.treesitter.language.get_lang(ft)
+    if not pcall(vim.treesitter.start, bufnr, lang) then
+      vim.bo[bufnr].syntax = ft
+    else
+    end
+  end
+
+  -- Replace the scratch buffer with a real buffer if we enter it
+  vim.api.nvim_create_autocmd("BufEnter", {
+    desc = "oil.nvim replace scratch buffer with real buffer",
+    buffer = bufnr,
+    callback = function()
+      local winid = vim.api.nvim_get_current_win()
+      -- Have to schedule this so all the FileType, etc autocmds will fire
+      vim.schedule(function()
+        if vim.api.nvim_get_current_win() == winid then
+          vim.cmd.edit({ args = { path } })
+
+          -- If we're still in a preview window, make sure this buffer still gets treated as a
+          -- preview
+          if vim.wo.previewwindow then
+            vim.bo.bufhidden = "wipe"
+            vim.b.oil_preview_buffer = true
+          end
+        end
+      end)
+    end,
+  })
+
+  return bufnr
+end
+
 return M

From 81cc9c3f62ddbef3687931d119e505643496fa0a Mon Sep 17 00:00:00 2001
From: cdmill <115658917+cdmill@users.noreply.github.com>
Date: Wed, 20 Nov 2024 22:06:09 -0700
Subject: [PATCH 110/206] feat: option to quite vim if oil is closed as last
 buffer (#491)

* feat: auto-quit vim if oil is closed as last buffer

* rename auto_close_vim to auto_close_last_buffer

* rework actions.close to be more like actions.cd

* fix: configure close action correctly

* add type annotation, future proofing

* fix: typo

* fix: typo

* refactor: better type annotations and backwards compatibility

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 lua/oil/actions.lua | 11 ++++++++++-
 lua/oil/init.lua    | 16 +++++++++++++---
 2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua
index 50a266a..8dabe40 100644
--- a/lua/oil/actions.lua
+++ b/lua/oil/actions.lua
@@ -143,7 +143,16 @@ M.parent = {
 
 M.close = {
   desc = "Close oil and restore original buffer",
-  callback = oil.close,
+  callback = function(opts)
+    opts = opts or {}
+    oil.close(opts)
+  end,
+  parameters = {
+    exit_if_last_buf = {
+      type = "boolean",
+      desc = "Exit vim if oil is closed as the last buffer",
+    },
+  },
 }
 
 ---@param cmd string
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index d673484..7645f35 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -379,8 +379,13 @@ M.open = function(dir)
   update_preview_window()
 end
 
+---@class oil.CloseOpts
+---@field exit_if_last_buf? boolean Exit vim if this oil buffer is the last open buffer
+
 ---Restore the buffer that was present when oil was opened
-M.close = function()
+---@param opts? oil.CloseOpts
+M.close = function(opts)
+  opts = opts or {}
   -- If we're in a floating oil window, close it and try to restore focus to the original window
   if vim.w.is_oil_win then
     local original_winid = vim.w.oil_original_win
@@ -403,9 +408,14 @@ M.close = function()
   -- buffer first
   local oilbuf = vim.api.nvim_get_current_buf()
   ok = pcall(vim.cmd.bprev)
+  -- If `bprev` failed, there are no buffers open
   if not ok then
-    -- If `bprev` failed, there are no buffers open so we should create a new one with enew
-    vim.cmd.enew()
+    -- either exit or create a new blank buffer
+    if opts.exit_if_last_buf then
+      vim.cmd.quit()
+    else
+      vim.cmd.enew()
+    end
   end
   vim.api.nvim_buf_delete(oilbuf, { force = true })
 end

From bf81e2a79a33d829226760781eaeeb553b8d0e4e Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Thu, 21 Nov 2024 05:06:28 +0000
Subject: [PATCH 111/206] [docgen] Update docs skip-checks: true

---
 README.md   |  2 +-
 doc/api.md  | 10 +++++++---
 doc/oil.txt |  9 ++++++++-
 3 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 0f4f3fe..e249c39 100644
--- a/README.md
+++ b/README.md
@@ -364,7 +364,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i
 - [open_float(dir)](doc/api.md#open_floatdir)
 - [toggle_float(dir)](doc/api.md#toggle_floatdir)
 - [open(dir)](doc/api.md#opendir)
-- [close()](doc/api.md#close)
+- [close(opts)](doc/api.md#closeopts)
 - [open_preview(opts, callback)](doc/api.md#open_previewopts-callback)
 - [select(opts, callback)](doc/api.md#selectopts-callback)
 - [save(opts, cb)](doc/api.md#saveopts-cb)
diff --git a/doc/api.md b/doc/api.md
index 35c6392..2f51cdd 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -13,7 +13,7 @@
 - [open_float(dir)](#open_floatdir)
 - [toggle_float(dir)](#toggle_floatdir)
 - [open(dir)](#opendir)
-- [close()](#close)
+- [close(opts)](#closeopts)
 - [open_preview(opts, callback)](#open_previewopts-callback)
 - [select(opts, callback)](#selectopts-callback)
 - [save(opts, cb)](#saveopts-cb)
@@ -119,11 +119,15 @@ Open oil browser for a directory
 | ----- | ------------- | ------------------------------------------------------------------------------------------- |
 | dir   | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
 
-## close()
+## close(opts)
 
-`close()` \
+`close(opts)` \
 Restore the buffer that was present when oil was opened
 
+| Param             | Type                 | Desc                                                |
+| ----------------- | -------------------- | --------------------------------------------------- |
+| opts              | `nil\|oil.CloseOpts` |                                                     |
+| >exit_if_last_buf | `nil\|boolean`       | Exit vim if this oil buffer is the last open buffer |
 
 ## open_preview(opts, callback)
 
diff --git a/doc/oil.txt b/doc/oil.txt
index 48df3a5..1ee0d11 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -316,9 +316,13 @@ open({dir})                                                             *oil.ope
       {dir} `nil|string` When nil, open the parent of the current buffer, or the
             cwd if current buffer is not a file
 
-close()                                                                *oil.close*
+close({opts})                                                          *oil.close*
     Restore the buffer that was present when oil was opened
 
+    Parameters:
+      {opts} `nil|oil.CloseOpts`
+          {exit_if_last_buf} `nil|boolean` Exit vim if this oil buffer is the
+                             last open buffer
 
 open_preview({opts}, {callback})                                *oil.open_preview*
     Preview the entry under the cursor in a split
@@ -513,6 +517,9 @@ change_sort                                                  *actions.change_sor
 close                                                              *actions.close*
     Close oil and restore original buffer
 
+    Parameters:
+      {exit_if_last_buf} `boolean` Exit vim if oil is closed as the last buffer
+
 open_cmdline                                                *actions.open_cmdline*
     Open vim cmdline with current entry as an argument
 

From 5acab3d8a9bc85a571688db432f2702dd7d901a4 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 21 Nov 2024 17:36:22 -0800
Subject: [PATCH 112/206] fix: image.nvim previews with preview_method=scratch

---
 lua/oil/init.lua |  6 +++++-
 lua/oil/util.lua | 22 ++++++++++++++++++++++
 2 files changed, 27 insertions(+), 1 deletion(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 7645f35..bbd290d 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -534,7 +534,11 @@ M.open_preview = function(opts, callback)
 
     local entry_is_file = not vim.endswith(normalized_url, "/")
     local filebufnr
-    if entry_is_file and config.preview_win.preview_method ~= "load" then
+    if
+      entry_is_file
+      and config.preview_win.preview_method ~= "load"
+      and not util.file_matches_bufreadcmd(normalized_url)
+    then
       filebufnr =
         util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method)
     end
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 24b714b..441421b 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -952,4 +952,26 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
   return bufnr
 end
 
+local _regcache = {}
+---Check if a file matches a BufReadCmd autocmd
+---@param filename string
+---@return boolean
+M.file_matches_bufreadcmd = function(filename)
+  local autocmds = vim.api.nvim_get_autocmds({
+    event = "BufReadCmd",
+  })
+  for _, au in ipairs(autocmds) do
+    local pat = _regcache[au.pattern]
+    if not pat then
+      pat = vim.fn.glob2regpat(au.pattern)
+      _regcache[au.pattern] = pat
+    end
+
+    if vim.fn.match(filename, pat) >= 0 then
+      return true
+    end
+  end
+  return false
+end
+
 return M

From 3fa3161aa9515ff6a7cf7e44458b6a2114262870 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 21 Nov 2024 17:36:40 -0800
Subject: [PATCH 113/206] feat: config option to disable previewing a file

---
 README.md          | 4 ++++
 doc/oil.txt        | 4 ++++
 lua/oil/config.lua | 6 ++++++
 lua/oil/init.lua   | 5 +++++
 4 files changed, 19 insertions(+)

diff --git a/README.md b/README.md
index e249c39..684ff24 100644
--- a/README.md
+++ b/README.md
@@ -274,6 +274,10 @@ require("oil").setup({
     update_on_cursor_moved = true,
     -- How to open the preview window "load"|"scratch"|"fast_scratch"
     preview_method = "fast_scratch",
+    -- A function that returns true to disable preview on a file e.g. to avoid lag
+    disable_preview = function(filename)
+      return false
+    end,
     -- Window-local options to use for preview window buffers
     win_options = {},
   },
diff --git a/doc/oil.txt b/doc/oil.txt
index 1ee0d11..0cfb872 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -159,6 +159,10 @@ CONFIG                                                                *oil-confi
         update_on_cursor_moved = true,
         -- How to open the preview window "load"|"scratch"|"fast_scratch"
         preview_method = "fast_scratch",
+        -- A function that returns true to disable preview on a file e.g. to avoid lag
+        disable_preview = function(filename)
+          return false
+        end,
         -- Window-local options to use for preview window buffers
         win_options = {},
       },
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 3b6b57e..185cb15 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -144,6 +144,10 @@ local default_config = {
     update_on_cursor_moved = true,
     -- How to open the preview window "load"|"scratch"|"fast_scratch"
     preview_method = "fast_scratch",
+    -- A function that returns true to disable preview on a file e.g. to avoid lag
+    disable_preview = function(filename)
+      return false
+    end,
     -- Window-local options to use for preview window buffers
     win_options = {},
   },
@@ -334,12 +338,14 @@ local M = {}
 ---@class (exact) oil.PreviewWindowConfig
 ---@field update_on_cursor_moved boolean
 ---@field preview_method oil.PreviewMethod
+---@field disable_preview fun(filename: string): boolean
 ---@field win_options table
 
 ---@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig
 
 ---@class (exact) oil.SetupPreviewWindowConfig
 ---@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved
+---@field disable_preview? fun(filename: string): boolean A function that returns true to disable preview on a file e.g. to avoid lag
 ---@field preview_method? oil.PreviewMethod How to open the preview window
 ---@field win_options? table Window-local options to use for preview window buffers
 
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index bbd290d..1450646 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -541,6 +541,11 @@ M.open_preview = function(opts, callback)
     then
       filebufnr =
         util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method)
+    elseif entry_is_file and config.preview_win.disable_preview(normalized_url) then
+      filebufnr = vim.api.nvim_create_buf(false, true)
+      vim.bo[filebufnr].bufhidden = "wipe"
+      vim.bo[filebufnr].buftype = "nofile"
+      util.render_text(filebufnr, "Preview disabled", { winid = preview_win })
     end
 
     if not filebufnr then

From 5fa528f5528bf04a2d255108e59ed9cf53e85ae6 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 21 Nov 2024 21:38:55 -0800
Subject: [PATCH 114/206] chore: refactor benchmarking to use benchmark.nvim

---
 .gitignore             |   3 +-
 Makefile               |  20 ++++---
 perf/bootstrap.lua     |  63 +++++++++++++++++++++
 tests/perf_harness.lua | 122 -----------------------------------------
 4 files changed, 76 insertions(+), 132 deletions(-)
 create mode 100644 perf/bootstrap.lua
 delete mode 100644 tests/perf_harness.lua

diff --git a/.gitignore b/.gitignore
index bb036c1..d427c40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,5 +48,6 @@ venv/
 doc/tags
 scripts/nvim_doc_tools
 scripts/nvim-typecheck-action
-tests/perf/
+scripts/benchmark.nvim
+perf/tmp/
 profile.json
diff --git a/Makefile b/Makefile
index bd7bf6c..10f01d1 100644
--- a/Makefile
+++ b/Makefile
@@ -37,20 +37,19 @@ fastlint: scripts/nvim_doc_tools venv
 
 ## profile: use LuaJIT profiler to profile the plugin
 .PHONY: profile
-profile:
-	nvim --clean -u tests/perf_harness.lua -c 'lua jit_profile()'
+profile: scripts/benchmark.nvim
+	nvim --clean -u perf/bootstrap.lua -c 'lua jit_profile()'
 
 ## flame_profile: create a trace in the chrome profiler format
 .PHONY: flame_profile
-flame_profile:
-	nvim --clean -u tests/perf_harness.lua -c 'lua flame_profile()'
-	@echo "Visit https://ui.perfetto.dev/ and load the profile.json file"
+flame_profile: scripts/benchmark.nvim
+	nvim --clean -u perf/bootstrap.lua -c 'lua flame_profile()'
 
 ## benchmark: benchmark performance opening directory with many files
 .PHONY: benchmark
-benchmark:
-	nvim --clean -u tests/perf_harness.lua -c 'lua benchmark(10)'
-	@cat tests/perf/benchmark.txt
+benchmark: scripts/benchmark.nvim
+	nvim --clean -u perf/bootstrap.lua -c 'lua benchmark()'
+	@cat perf/tmp/benchmark.txt
 
 scripts/nvim_doc_tools:
 	git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools
@@ -58,7 +57,10 @@ scripts/nvim_doc_tools:
 scripts/nvim-typecheck-action:
 	git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action
 
+scripts/benchmark.nvim:
+	git clone https://github.com/stevearc/benchmark.nvim scripts/benchmark.nvim
+
 ## clean: reset the repository to a clean state
 .PHONY: clean
 clean:
-	rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv tests/perf profile.json
+	rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv perf/tmp profile.json
diff --git a/perf/bootstrap.lua b/perf/bootstrap.lua
new file mode 100644
index 0000000..5f10c06
--- /dev/null
+++ b/perf/bootstrap.lua
@@ -0,0 +1,63 @@
+vim.opt.runtimepath:prepend("scripts/benchmark.nvim")
+vim.opt.runtimepath:prepend(".")
+
+local bm = require("benchmark")
+bm.sandbox()
+
+---@module 'oil'
+---@type oil.SetupOpts
+local setup_opts = {
+  -- columns = { "icon", "permissions", "size", "mtime" },
+}
+
+local DIR_SIZE = tonumber(vim.env.DIR_SIZE) or 100000
+local ITERATIONS = tonumber(vim.env.ITERATIONS) or 10
+local WARM_UP = tonumber(vim.env.WARM_UP) or 1
+local OUTLIERS = tonumber(vim.env.OUTLIERS) or math.floor(ITERATIONS / 10)
+local TEST_DIR = "perf/tmp/test_" .. DIR_SIZE
+
+vim.fn.mkdir(TEST_DIR, "p")
+require("benchmark.files").create_files(TEST_DIR, "file %d.txt", DIR_SIZE)
+
+function _G.jit_profile()
+  require("oil").setup(setup_opts)
+  local finish = bm.jit_profile({ filename = TEST_DIR .. "/profile.txt" })
+  bm.wait_for_user_event("OilEnter", function()
+    finish()
+  end)
+  require("oil").open(TEST_DIR)
+end
+
+function _G.flame_profile()
+  local start, stop = bm.flame_profile({
+    pattern = "oil*",
+    filename = "profile.json",
+  })
+  require("oil").setup(setup_opts)
+  start()
+  bm.wait_for_user_event("OilEnter", function()
+    stop(function()
+      vim.cmd.qall({ mods = { silent = true } })
+    end)
+  end)
+  require("oil").open(TEST_DIR)
+end
+
+function _G.benchmark()
+  require("oil").setup(setup_opts)
+  bm.run({ title = "oil.nvim", iterations = ITERATIONS, warm_up = WARM_UP }, function(callback)
+    bm.wait_for_user_event("OilEnter", callback)
+    require("oil").open(TEST_DIR)
+  end, function(times)
+    local avg = bm.avg(times, { trim_outliers = OUTLIERS })
+    local std_dev = bm.std_dev(times, { trim_outliers = OUTLIERS })
+    local lines = {
+      table.concat(vim.tbl_map(bm.format_time, times), " "),
+      string.format("Average: %s", bm.format_time(avg)),
+      string.format("Std deviation: %s", bm.format_time(std_dev)),
+    }
+
+    vim.fn.writefile(lines, "perf/tmp/benchmark.txt")
+    vim.cmd.qall({ mods = { silent = true } })
+  end)
+end
diff --git a/tests/perf_harness.lua b/tests/perf_harness.lua
deleted file mode 100644
index 679004d..0000000
--- a/tests/perf_harness.lua
+++ /dev/null
@@ -1,122 +0,0 @@
-vim.fn.mkdir("tests/perf/.env", "p")
-local root = vim.fn.fnamemodify("./tests/perf/.env", ":p")
-
-for _, name in ipairs({ "config", "data", "state", "runtime", "cache" }) do
-  vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. name
-end
-
-vim.opt.runtimepath:prepend(vim.fn.fnamemodify(".", ":p"))
-
----@module 'oil'
----@type oil.SetupOpts
-local setup_opts = {
-  -- columns = { "icon", "permissions", "size", "mtime" },
-}
-
-local num_files = 100000
-
-if not vim.uv.fs_stat(string.format("tests/perf/file %d.txt", num_files)) then
-  vim.notify("Creating files")
-  for i = 1, num_files, 1 do
-    local filename = ("tests/perf/file %d.txt"):format(i)
-    local fd = vim.uv.fs_open(filename, "a", 420)
-    assert(fd)
-    vim.uv.fs_close(fd)
-  end
-end
-
-local function wait_for_done(callback)
-  vim.api.nvim_create_autocmd("User", {
-    pattern = "OilEnter",
-    once = true,
-    callback = callback,
-  })
-end
-
-function _G.jit_profile()
-  require("oil").setup(setup_opts)
-  local outfile = "tests/perf/profile.txt"
-  require("jit.p").start("3Fpli1s", outfile)
-  local start = vim.uv.hrtime()
-  require("oil").open("tests/perf")
-
-  wait_for_done(function()
-    local delta = vim.uv.hrtime() - start
-    require("jit.p").stop()
-    print("Elapsed:", delta / 1e6, "ms")
-    vim.cmd.edit({ args = { outfile } })
-  end)
-end
-
-function _G.benchmark(iterations)
-  require("oil").setup(setup_opts)
-  local num_outliers = math.floor(0.1 * iterations)
-  local times = {}
-
-  local run_profile
-  run_profile = function()
-    -- Clear out state
-    vim.cmd.enew()
-    for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
-      if vim.api.nvim_buf_is_valid(bufnr) and bufnr ~= vim.api.nvim_get_current_buf() then
-        vim.api.nvim_buf_delete(bufnr, { force = true })
-      end
-    end
-
-    local start = vim.uv.hrtime()
-    wait_for_done(function()
-      local delta = vim.uv.hrtime() - start
-      table.insert(times, delta / 1e6)
-      if #times < iterations then
-        vim.schedule(run_profile)
-      else
-        -- Remove the outliers
-        table.sort(times)
-        for _ = 1, num_outliers do
-          table.remove(times, 1)
-          table.remove(times)
-        end
-
-        local total = 0
-        for _, time in ipairs(times) do
-          total = total + time
-        end
-
-        local lines = {
-          table.concat(
-            vim.tbl_map(function(t)
-              return string.format("%dms", math.floor(t))
-            end, times),
-            " "
-          ),
-          string.format("Average: %dms", math.floor(total / #times)),
-        }
-        vim.fn.writefile(lines, "tests/perf/benchmark.txt")
-        vim.cmd.qall()
-      end
-    end)
-    require("oil").open("tests/perf")
-  end
-
-  run_profile()
-end
-
-function _G.flame_profile()
-  if not vim.uv.fs_stat("tests/perf/profile.nvim") then
-    vim
-      .system({ "git", "clone", "https://github.com/stevearc/profile.nvim", "tests/perf/profile.nvim" })
-      :wait()
-  end
-  vim.opt.runtimepath:prepend(vim.fn.fnamemodify("./tests/perf/profile.nvim", ":p"))
-  local profile = require("profile")
-  profile.instrument_autocmds()
-  profile.instrument("oil*")
-
-  require("oil").setup(setup_opts)
-  profile.start()
-  require("oil").open("tests/perf")
-  wait_for_done(function()
-    profile.stop("profile.json")
-    vim.cmd.qall()
-  end)
-end

From 740b8fd425a2b77f7f40eb5ac155ebe66ff9515c Mon Sep 17 00:00:00 2001
From: Foo-x 
Date: Sat, 23 Nov 2024 01:17:50 +0900
Subject: [PATCH 115/206] feat: add highlight group for orphaned links (#502)

* feat: add highlight for orphan links

Closes #501

* feat: add OilOrphanLinkTarget highlight group

---------

Co-authored-by: Steven Arcangeli 
---
 doc/oil.txt      |  6 ++++++
 lua/oil/init.lua | 10 ++++++++++
 lua/oil/view.lua |  5 +++--
 3 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index 0cfb872..62e547e 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -619,9 +619,15 @@ OilSocket                                                           *hl-OilSocke
 OilLink                                                               *hl-OilLink*
     Soft links in an oil buffer
 
+OilOrphanLink                                                   *hl-OilOrphanLink*
+    Orphaned soft links in an oil buffer
+
 OilLinkTarget                                                   *hl-OilLinkTarget*
     The target of a soft link
 
+OilOrphanLinkTarget                                       *hl-OilOrphanLinkTarget*
+    The target of an orphaned soft link
+
 OilFile                                                               *hl-OilFile*
     Normal files in an oil buffer
 
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 1450646..e8b8275 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -816,11 +816,21 @@ M._get_highlights = function()
       link = nil,
       desc = "Soft links in an oil buffer",
     },
+    {
+      name = "OilOrphanLink",
+      link = nil,
+      desc = "Orphaned soft links in an oil buffer",
+    },
     {
       name = "OilLinkTarget",
       link = "Comment",
       desc = "The target of a soft link",
     },
+    {
+      name = "OilOrphanLinkTarget",
+      link = "DiagnosticError",
+      desc = "The target of an orphaned soft link",
+    },
     {
       name = "OilFile",
       link = nil,
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index b36140b..6a5939d 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -728,10 +728,11 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
         end
       end
     end
+    local is_orphan = not (meta and meta.link_stat)
 
-    table.insert(cols, { name, "OilLink" })
+    table.insert(cols, { name, is_orphan and "OilOrphanLink" or "OilLink" })
     if link_text then
-      table.insert(cols, { link_text, "OilLinkTarget" })
+      table.insert(cols, { link_text, is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget" })
     end
   else
     table.insert(cols, { name, "OilFile" })

From 60e68967e51ff1ecd264c29e3de0d52bfff22df3 Mon Sep 17 00:00:00 2001
From: Ezekiel Warren 
Date: Fri, 22 Nov 2024 08:55:55 -0800
Subject: [PATCH 116/206] feat: highlight groups for hidden files (#459)

* feat: hidden highlights

* feat: OilHidden for hidden highlights instead of Comment

* fix: add the new combinatoric highlight groups

* perf: get rid of a call to is_hidden_file

* fix: tweak the default highlight group links

* fix: update function call in unit tests

---------

Co-authored-by: Steven Arcangeli 
---
 doc/oil.txt           | 24 ++++++++++++++++++++++++
 lua/oil/init.lua      | 40 ++++++++++++++++++++++++++++++++++++++++
 lua/oil/view.lua      | 40 +++++++++++++++++++++++++++-------------
 tests/parser_spec.lua |  2 +-
 4 files changed, 92 insertions(+), 14 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index 62e547e..87d4bbb 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -607,30 +607,54 @@ yank_entry                                                    *actions.yank_entr
 --------------------------------------------------------------------------------
 HIGHLIGHTS                                                        *oil-highlights*
 
+OilHidden                                                           *hl-OilHidden*
+    Hidden entry in an oil buffer
+
 OilDir                                                                 *hl-OilDir*
     Directory names in an oil buffer
 
+OilDirHidden                                                     *hl-OilDirHidden*
+    Hidden directory names in an oil buffer
+
 OilDirIcon                                                         *hl-OilDirIcon*
     Icon for directories
 
 OilSocket                                                           *hl-OilSocket*
     Socket files in an oil buffer
 
+OilSocketHidden                                               *hl-OilSocketHidden*
+    Hidden socket files in an oil buffer
+
 OilLink                                                               *hl-OilLink*
     Soft links in an oil buffer
 
 OilOrphanLink                                                   *hl-OilOrphanLink*
     Orphaned soft links in an oil buffer
 
+OilLinkHidden                                                   *hl-OilLinkHidden*
+    Hidden soft links in an oil buffer
+
+OilOrphanLinkHidden                                       *hl-OilOrphanLinkHidden*
+    Hidden orphaned soft links in an oil buffer
+
 OilLinkTarget                                                   *hl-OilLinkTarget*
     The target of a soft link
 
 OilOrphanLinkTarget                                       *hl-OilOrphanLinkTarget*
     The target of an orphaned soft link
 
+OilLinkTargetHidden                                       *hl-OilLinkTargetHidden*
+    The target of a hidden soft link
+
+OilOrphanLinkTargetHidden                           *hl-OilOrphanLinkTargetHidden*
+    The target of an hidden orphaned soft link
+
 OilFile                                                               *hl-OilFile*
     Normal files in an oil buffer
 
+OilFileHidden                                                   *hl-OilFileHidden*
+    Hidden normal files in an oil buffer
+
 OilCreate                                                           *hl-OilCreate*
     Create action in the oil preview window
 
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index e8b8275..72d5538 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -796,11 +796,21 @@ end
 ---@private
 M._get_highlights = function()
   return {
+    {
+      name = "OilHidden",
+      link = "Comment",
+      desc = "Hidden entry in an oil buffer",
+    },
     {
       name = "OilDir",
       link = "Directory",
       desc = "Directory names in an oil buffer",
     },
+    {
+      name = "OilDirHidden",
+      link = "OilHidden",
+      desc = "Hidden directory names in an oil buffer",
+    },
     {
       name = "OilDirIcon",
       link = "OilDir",
@@ -811,6 +821,11 @@ M._get_highlights = function()
       link = "Keyword",
       desc = "Socket files in an oil buffer",
     },
+    {
+      name = "OilSocketHidden",
+      link = "OilHidden",
+      desc = "Hidden socket files in an oil buffer",
+    },
     {
       name = "OilLink",
       link = nil,
@@ -821,6 +836,16 @@ M._get_highlights = function()
       link = nil,
       desc = "Orphaned soft links in an oil buffer",
     },
+    {
+      name = "OilLinkHidden",
+      link = "OilHidden",
+      desc = "Hidden soft links in an oil buffer",
+    },
+    {
+      name = "OilOrphanLinkHidden",
+      link = "OilLinkHidden",
+      desc = "Hidden orphaned soft links in an oil buffer",
+    },
     {
       name = "OilLinkTarget",
       link = "Comment",
@@ -831,11 +856,26 @@ M._get_highlights = function()
       link = "DiagnosticError",
       desc = "The target of an orphaned soft link",
     },
+    {
+      name = "OilLinkTargetHidden",
+      link = "OilHidden",
+      desc = "The target of a hidden soft link",
+    },
+    {
+      name = "OilOrphanLinkTargetHidden",
+      link = "OilOrphanLinkTarget",
+      desc = "The target of an hidden orphaned soft link",
+    },
     {
       name = "OilFile",
       link = nil,
       desc = "Normal files in an oil buffer",
     },
+    {
+      name = "OilFileHidden",
+      link = "OilHidden",
+      desc = "Hidden normal files in an oil buffer",
+    },
     {
       name = "OilCreate",
       link = "DiagnosticInfo",
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 6a5939d..74ab145 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -19,10 +19,16 @@ local last_cursor_entry = {}
 
 ---@param name string
 ---@param bufnr integer
----@return boolean
+---@return boolean display
+---@return boolean is_hidden Whether the file is classified as a hidden file
 M.should_display = function(name, bufnr)
-  return not config.view_options.is_always_hidden(name, bufnr)
-    and (config.view_options.show_hidden or not config.view_options.is_hidden_file(name, bufnr))
+  if config.view_options.is_always_hidden(name, bufnr) then
+    return false, true
+  else
+    local is_hidden = config.view_options.is_hidden_file(name, bufnr)
+    local display = config.view_options.show_hidden or not is_hidden
+    return display, is_hidden
+  end
 end
 
 ---@param bufname string
@@ -633,13 +639,15 @@ local function render_buffer(bufnr, opts)
   end
 
   if M.should_display("..", bufnr) then
-    local cols = M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter)
+    local cols =
+      M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter, true)
     table.insert(line_table, cols)
   end
 
   for _, entry in ipairs(entry_list) do
-    if M.should_display(entry[FIELD_NAME], bufnr) then
-      local cols = M.format_entry_cols(entry, column_defs, col_width, adapter)
+    local should_display, is_hidden = M.should_display(entry[FIELD_NAME], bufnr)
+    if should_display then
+      local cols = M.format_entry_cols(entry, column_defs, col_width, adapter, is_hidden)
       table.insert(line_table, cols)
 
       local name = entry[FIELD_NAME]
@@ -688,10 +696,15 @@ end
 ---@param column_defs table[]
 ---@param col_width integer[]
 ---@param adapter oil.Adapter
+---@param is_hidden boolean
 ---@return oil.TextChunk[]
-M.format_entry_cols = function(entry, column_defs, col_width, adapter)
+M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden)
   local name = entry[FIELD_NAME]
   local meta = entry[FIELD_META]
+  local hl_suffix = ""
+  if is_hidden then
+    hl_suffix = "Hidden"
+  end
   if meta and meta.display_name then
     name = meta.display_name
   end
@@ -711,9 +724,9 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
   -- Always add the entry name at the end
   local entry_type = entry[FIELD_TYPE]
   if entry_type == "directory" then
-    table.insert(cols, { name .. "/", "OilDir" })
+    table.insert(cols, { name .. "/", "OilDir" .. hl_suffix })
   elseif entry_type == "socket" then
-    table.insert(cols, { name, "OilSocket" })
+    table.insert(cols, { name, "OilSocket" .. hl_suffix })
   elseif entry_type == "link" then
     local link_text
     if meta then
@@ -722,7 +735,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
       end
 
       if meta.link then
-        link_text = "->" .. " " .. meta.link
+        link_text = "-> " .. meta.link
         if meta.link_stat and meta.link_stat.type == "directory" then
           link_text = util.addslash(link_text)
         end
@@ -730,12 +743,13 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
     end
     local is_orphan = not (meta and meta.link_stat)
 
-    table.insert(cols, { name, is_orphan and "OilOrphanLink" or "OilLink" })
+    table.insert(cols, { name, (is_orphan and "OilOrphanLink" or "OilLink") .. hl_suffix })
     if link_text then
-      table.insert(cols, { link_text, is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget" })
+      local target_hl = (is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget") .. hl_suffix
+      table.insert(cols, { link_text, target_hl })
     end
   else
-    table.insert(cols, { name, "OilFile" })
+    table.insert(cols, { name, "OilFile" .. hl_suffix })
   end
   return cols
 end
diff --git a/tests/parser_spec.lua b/tests/parser_spec.lua
index 527e821..9884ca1 100644
--- a/tests/parser_spec.lua
+++ b/tests/parser_spec.lua
@@ -90,7 +90,7 @@ describe("parser", function()
     local file = test_adapter.test_set("/foo/a.txt", "file")
     vim.cmd.edit({ args = { "oil-test:///foo/" } })
     local bufnr = vim.api.nvim_get_current_buf()
-    local cols = view.format_entry_cols(file, {}, {}, test_adapter)
+    local cols = view.format_entry_cols(file, {}, {}, test_adapter, false)
     local lines = util.render_table({ cols }, {})
     table.insert(lines, "")
     table.insert(lines, "     ")

From 99ce32f4a2ecf76263b72fcc31efb163faa1a941 Mon Sep 17 00:00:00 2001
From: Foo-x 
Date: Sat, 23 Nov 2024 03:23:08 +0900
Subject: [PATCH 117/206] feat: config option to customize filename highlight
 group (#508)

* feat: highlight config

Refs #402

* perf: minimize perf impact when option not provided

* doc: regenerate documentation

* fix: symbolic link rendering

* refactor: simplify conditional

---------

Co-authored-by: Steven Arcangeli 
---
 README.md          |  4 +++
 doc/oil.txt        |  4 +++
 lua/oil/config.lua |  9 ++++++
 lua/oil/view.lua   | 75 ++++++++++++++++++++++++++++++++++++----------
 4 files changed, 76 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md
index 684ff24..f690904 100644
--- a/README.md
+++ b/README.md
@@ -232,6 +232,10 @@ require("oil").setup({
       { "type", "asc" },
       { "name", "asc" },
     },
+    -- Customize the highlight group for the file name
+    highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
+      return nil
+    end,
   },
   -- Extra arguments to pass to SCP when moving/copying files over SSH
   extra_scp_args = {},
diff --git a/doc/oil.txt b/doc/oil.txt
index 87d4bbb..63bca11 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -117,6 +117,10 @@ CONFIG                                                                *oil-confi
           { "type", "asc" },
           { "name", "asc" },
         },
+        -- Customize the highlight group for the file name
+        highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
+          return nil
+        end,
       },
       -- Extra arguments to pass to SCP when moving/copying files over SSH
       extra_scp_args = {},
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 185cb15..0374e43 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -102,6 +102,10 @@ local default_config = {
       { "type", "asc" },
       { "name", "asc" },
     },
+    -- Customize the highlight group for the file name
+    highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
+      return nil
+    end,
   },
   -- Extra arguments to pass to SCP when moving/copying files over SSH
   extra_scp_args = {},
@@ -207,6 +211,9 @@ default_config.adapters = {
   ["oil-trash://"] = "trash",
 }
 default_config.adapter_aliases = {}
+-- We want the function in the default config for documentation generation, but if we nil it out
+-- here we can get some performance wins
+default_config.view_options.highlight_filename = nil
 
 ---@class oil.Config
 ---@field adapters table Hidden from SetupOpts
@@ -281,6 +288,7 @@ local M = {}
 ---@field natural_order boolean|"fast"
 ---@field case_insensitive boolean
 ---@field sort oil.SortSpec[]
+---@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean): string|nil
 
 ---@class (exact) oil.SetupViewOptions
 ---@field show_hidden? boolean Show files and directories that start with "."
@@ -289,6 +297,7 @@ local M = {}
 ---@field natural_order? boolean|"fast" Sort file names with numbers in a more intuitive order for humans. Can be slow for large directories.
 ---@field case_insensitive? boolean Sort file and directory names case insensitive
 ---@field sort? oil.SortSpec[] Sort order for the file list
+---@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean): string|nil Customize the highlight group for the file name
 
 ---@class (exact) oil.SortSpec
 ---@field [1] string
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 74ab145..6b26182 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -691,6 +691,28 @@ local function render_buffer(bufnr, opts)
   return seek_after_render_found
 end
 
+---@param name string
+---@param meta? table
+---@return string filename
+---@return string|nil link_target
+local function get_link_text(name, meta)
+  local link_text
+  if meta then
+    if meta.link_stat and meta.link_stat.type == "directory" then
+      name = name .. "/"
+    end
+
+    if meta.link then
+      link_text = "-> " .. meta.link
+      if meta.link_stat and meta.link_stat.type == "directory" then
+        link_text = util.addslash(link_text)
+      end
+    end
+  end
+
+  return name, link_text
+end
+
 ---@private
 ---@param entry oil.InternalEntry
 ---@param column_defs table[]
@@ -723,34 +745,55 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
   end
   -- Always add the entry name at the end
   local entry_type = entry[FIELD_TYPE]
+
+  local get_custom_hl = config.view_options.highlight_filename
+  local link_name, link_name_hl, link_target, link_target_hl
+  if get_custom_hl then
+    local external_entry = util.export_entry(entry)
+
+    if entry_type == "link" then
+      link_name, link_target = get_link_text(name, meta)
+      local is_orphan = not (meta and meta.link_stat)
+      link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan)
+
+      if link_target then
+        link_target_hl = get_custom_hl(external_entry, is_hidden, true, is_orphan)
+      end
+
+      -- intentional fallthrough
+    else
+      local hl = get_custom_hl(external_entry, is_hidden, false, false)
+      if hl then
+        table.insert(cols, { name, hl })
+        return cols
+      end
+    end
+  end
+
   if entry_type == "directory" then
     table.insert(cols, { name .. "/", "OilDir" .. hl_suffix })
   elseif entry_type == "socket" then
     table.insert(cols, { name, "OilSocket" .. hl_suffix })
   elseif entry_type == "link" then
-    local link_text
-    if meta then
-      if meta.link_stat and meta.link_stat.type == "directory" then
-        name = name .. "/"
-      end
-
-      if meta.link then
-        link_text = "-> " .. meta.link
-        if meta.link_stat and meta.link_stat.type == "directory" then
-          link_text = util.addslash(link_text)
-        end
-      end
+    if not link_name then
+      link_name, link_target = get_link_text(name, meta)
     end
     local is_orphan = not (meta and meta.link_stat)
+    if not link_name_hl then
+      link_name_hl = (is_orphan and "OilOrphanLink" or "OilLink") .. hl_suffix
+    end
+    table.insert(cols, { link_name, link_name_hl })
 
-    table.insert(cols, { name, (is_orphan and "OilOrphanLink" or "OilLink") .. hl_suffix })
-    if link_text then
-      local target_hl = (is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget") .. hl_suffix
-      table.insert(cols, { link_text, target_hl })
+    if link_target then
+      if not link_target_hl then
+        link_target_hl = (is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget") .. hl_suffix
+      end
+      table.insert(cols, { link_target, link_target_hl })
     end
   else
     table.insert(cols, { name, "OilFile" .. hl_suffix })
   end
+
   return cols
 end
 

From da93d55e32d73a17c447067d168d80290ae96590 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sun, 24 Nov 2024 15:04:00 -0800
Subject: [PATCH 118/206] fix: work around performance issue with treesitter,
 folds, and large directories

---
 lua/oil/view.lua | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 6b26182..15e5022 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -185,6 +185,10 @@ end
 
 M.set_win_options = function()
   local winid = vim.api.nvim_get_current_win()
+
+  -- work around https://github.com/neovim/neovim/pull/27422
+  vim.api.nvim_set_option_value("foldmethod", "manual", { scope = "local", win = winid })
+
   for k, v in pairs(config.win_options) do
     vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid })
   end

From 3c2de37accead0240fbe812f5ccdedfe0b973557 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Mon, 25 Nov 2024 09:10:29 -0800
Subject: [PATCH 119/206] debug: include shell command in error message

---
 lua/oil/shell.lua | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lua/oil/shell.lua b/lua/oil/shell.lua
index 2c401ef..b04b27b 100644
--- a/lua/oil/shell.lua
+++ b/lua/oil/shell.lua
@@ -26,7 +26,8 @@ M.run = function(cmd, opts, callback)
           if err == "" then
             err = "Unknown error"
           end
-          callback(err)
+          local cmd_str = type(cmd) == "string" and cmd or table.concat(cmd, " ")
+          callback(string.format("Error running command '%s'\n%s", cmd_str, err))
         end
       end),
     })

From f2b324933f4d505cff6f7d445fd61fad02dcd9ae Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Tue, 3 Dec 2024 09:44:07 -0800
Subject: [PATCH 120/206] feat: better merging of action desc when overriding
 keymaps

---
 lua/oil/config.lua      | 26 ++++++++++++--------------
 lua/oil/keymap_util.lua | 13 ++++++++++++-
 2 files changed, 24 insertions(+), 15 deletions(-)

diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 0374e43..9102727 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -1,5 +1,3 @@
---stylua: ignore
-
 local default_config = {
   -- Oil will take over directory buffers (e.g. `vim .` or `:e src/`)
   -- Set to false if you want some other plugin (e.g. netrw) to open when you edit directories.
@@ -60,22 +58,22 @@ local default_config = {
   -- Set to `false` to remove a keymap
   -- See :help oil-actions for a list of all available actions
   keymaps = {
-    ["g?"] = "actions.show_help",
+    ["g?"] = { "actions.show_help", mode = "n" },
     [""] = "actions.select",
-    [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" },
-    [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" },
-    [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
+    [""] = { "actions.select", opts = { vertical = true } },
+    [""] = { "actions.select", opts = { horizontal = true } },
+    [""] = { "actions.select", opts = { tab = true } },
     [""] = "actions.preview",
-    [""] = "actions.close",
+    [""] = { "actions.close", mode = "n" },
     [""] = "actions.refresh",
-    ["-"] = "actions.parent",
-    ["_"] = "actions.open_cwd",
-    ["`"] = "actions.cd",
-    ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory", mode = "n" },
-    ["gs"] = "actions.change_sort",
+    ["-"] = { "actions.parent", mode = "n" },
+    ["_"] = { "actions.open_cwd", mode = "n" },
+    ["`"] = { "actions.cd", mode = "n" },
+    ["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
+    ["gs"] = { "actions.change_sort", mode = "n" },
     ["gx"] = "actions.open_external",
-    ["g."] = "actions.toggle_hidden",
-    ["g\\"] = "actions.toggle_trash",
+    ["g."] = { "actions.toggle_hidden", mode = "n" },
+    ["g\\"] = { "actions.toggle_trash", mode = "n" },
   },
   -- Set to false to disable all of the above keymaps
   use_default_keymaps = true,
diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua
index b62756e..04b8066 100644
--- a/lua/oil/keymap_util.lua
+++ b/lua/oil/keymap_util.lua
@@ -19,7 +19,18 @@ local function resolve(rhs)
   elseif type(rhs) == "table" then
     local opts = vim.deepcopy(rhs)
     -- We support passing in a `callback` key, or using the 1 index as the rhs of the keymap
-    local callback = resolve(opts.callback or opts[1])
+    local callback, parent_opts = resolve(opts.callback or opts[1])
+
+    -- Fall back to the parent desc, adding the opts as a string if it exists
+    if parent_opts.desc and not opts.desc then
+      if opts.opts then
+        opts.desc =
+          string.format("%s %s", parent_opts.desc, vim.inspect(opts.opts):gsub("%s+", " "))
+      else
+        opts.desc = parent_opts.desc
+      end
+    end
+
     local mode = opts.mode
     if type(rhs.callback) == "string" then
       local action_opts, action_mode

From 9a59256c8e88b29d2150e99b5960b2f111e51f75 Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Tue, 3 Dec 2024 17:45:14 +0000
Subject: [PATCH 121/206] [docgen] Update docs skip-checks: true

---
 README.md   | 24 ++++++++++++------------
 doc/oil.txt | 24 ++++++++++++------------
 2 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/README.md b/README.md
index f690904..0b8d0e5 100644
--- a/README.md
+++ b/README.md
@@ -190,22 +190,22 @@ require("oil").setup({
   -- Set to `false` to remove a keymap
   -- See :help oil-actions for a list of all available actions
   keymaps = {
-    ["g?"] = "actions.show_help",
+    ["g?"] = { "actions.show_help", mode = "n" },
     [""] = "actions.select",
-    [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" },
-    [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" },
-    [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
+    [""] = { "actions.select", opts = { vertical = true } },
+    [""] = { "actions.select", opts = { horizontal = true } },
+    [""] = { "actions.select", opts = { tab = true } },
     [""] = "actions.preview",
-    [""] = "actions.close",
+    [""] = { "actions.close", mode = "n" },
     [""] = "actions.refresh",
-    ["-"] = "actions.parent",
-    ["_"] = "actions.open_cwd",
-    ["`"] = "actions.cd",
-    ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory", mode = "n" },
-    ["gs"] = "actions.change_sort",
+    ["-"] = { "actions.parent", mode = "n" },
+    ["_"] = { "actions.open_cwd", mode = "n" },
+    ["`"] = { "actions.cd", mode = "n" },
+    ["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
+    ["gs"] = { "actions.change_sort", mode = "n" },
     ["gx"] = "actions.open_external",
-    ["g."] = "actions.toggle_hidden",
-    ["g\\"] = "actions.toggle_trash",
+    ["g."] = { "actions.toggle_hidden", mode = "n" },
+    ["g\\"] = { "actions.toggle_trash", mode = "n" },
   },
   -- Set to false to disable all of the above keymaps
   use_default_keymaps = true,
diff --git a/doc/oil.txt b/doc/oil.txt
index 63bca11..8540cbd 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -75,22 +75,22 @@ CONFIG                                                                *oil-confi
       -- Set to `false` to remove a keymap
       -- See :help oil-actions for a list of all available actions
       keymaps = {
-        ["g?"] = "actions.show_help",
+        ["g?"] = { "actions.show_help", mode = "n" },
         [""] = "actions.select",
-        [""] = { "actions.select", opts = { vertical = true }, desc = "Open the entry in a vertical split" },
-        [""] = { "actions.select", opts = { horizontal = true }, desc = "Open the entry in a horizontal split" },
-        [""] = { "actions.select", opts = { tab = true }, desc = "Open the entry in new tab" },
+        [""] = { "actions.select", opts = { vertical = true } },
+        [""] = { "actions.select", opts = { horizontal = true } },
+        [""] = { "actions.select", opts = { tab = true } },
         [""] = "actions.preview",
-        [""] = "actions.close",
+        [""] = { "actions.close", mode = "n" },
         [""] = "actions.refresh",
-        ["-"] = "actions.parent",
-        ["_"] = "actions.open_cwd",
-        ["`"] = "actions.cd",
-        ["~"] = { "actions.cd", opts = { scope = "tab" }, desc = ":tcd to the current oil directory", mode = "n" },
-        ["gs"] = "actions.change_sort",
+        ["-"] = { "actions.parent", mode = "n" },
+        ["_"] = { "actions.open_cwd", mode = "n" },
+        ["`"] = { "actions.cd", mode = "n" },
+        ["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
+        ["gs"] = { "actions.change_sort", mode = "n" },
         ["gx"] = "actions.open_external",
-        ["g."] = "actions.toggle_hidden",
-        ["g\\"] = "actions.toggle_trash",
+        ["g."] = { "actions.toggle_hidden", mode = "n" },
+        ["g\\"] = { "actions.toggle_trash", mode = "n" },
       },
       -- Set to false to disable all of the above keymaps
       use_default_keymaps = true,

From 7a55ede5e745e31ea8e4cb5483221524922294bf Mon Sep 17 00:00:00 2001
From: lucascool12 <71119084+lucascool12@users.noreply.github.com>
Date: Tue, 10 Dec 2024 17:08:43 +0100
Subject: [PATCH 122/206] fix: improper file name escaping (#530)

---
 lua/oil/util.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 441421b..6dc85a8 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -21,7 +21,7 @@ end
 ---@param filename string
 ---@return string
 M.escape_filename = function(filename)
-  local ret = filename:gsub("([%%#$])", "\\%1")
+  local ret = vim.fn.fnameescape(filename)
   return ret
 end
 

From dba037598843973b8c54bc5ce0318db4a0da439d Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Tue, 10 Dec 2024 15:22:19 -0800
Subject: [PATCH 123/206] fix: handle files with newlines in the name (#534)

---
 lua/oil/mutator/confirmation.lua | 2 ++
 lua/oil/view.lua                 | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/lua/oil/mutator/confirmation.lua b/lua/oil/mutator/confirmation.lua
index 4e86abc..8bc8020 100644
--- a/lua/oil/mutator/confirmation.lua
+++ b/lua/oil/mutator/confirmation.lua
@@ -82,6 +82,8 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
     else
       line = adapter.render_action(action)
     end
+    -- We can't handle lines with newlines in them
+    line = line:gsub("\n", "")
     table.insert(lines, line)
     local line_width = vim.api.nvim_strwidth(line)
     if line_width > max_line_width then
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 15e5022..c6d9603 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -734,6 +734,8 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
   if meta and meta.display_name then
     name = meta.display_name
   end
+  -- We can't handle newlines in filenames (and shame on you for doing that)
+  name = name:gsub("\n", "")
   -- First put the unique ID
   local cols = {}
   local id_key = cache.format_id(entry[FIELD_ID])

From 78ab7ca1073731ebdf82efa474202defa028d5a4 Mon Sep 17 00:00:00 2001
From: Gustavo Sampaio 
Date: Sat, 21 Dec 2024 01:15:47 -0300
Subject: [PATCH 124/206] fix: don't take over the preview window until it's
 opened for oil (#532)

---
 lua/oil/init.lua |  4 +++-
 lua/oil/util.lua | 11 +++++++++--
 2 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 72d5538..275c97d 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -454,7 +454,7 @@ M.open_preview = function(opts, callback)
     end
   end
 
-  local preview_win = util.get_preview_win()
+  local preview_win = util.get_preview_win({ include_not_owned = true })
   local prev_win = vim.api.nvim_get_current_win()
   local bufnr = vim.api.nvim_get_current_buf()
 
@@ -501,6 +501,7 @@ M.open_preview = function(opts, callback)
 
       preview_win = vim.api.nvim_open_win(bufnr, true, win_opts)
       vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = preview_win })
+      vim.api.nvim_win_set_var(preview_win, "oil_preview", true)
       vim.api.nvim_set_current_win(prev_win)
     elseif vim.fn.has("nvim-0.9") == 1 then
       vim.api.nvim_win_set_config(preview_win, { title = entry_title })
@@ -568,6 +569,7 @@ M.open_preview = function(opts, callback)
     end
 
     vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 })
+    vim.api.nvim_win_set_var(0, "oil_preview", true)
     for k, v in pairs(config.preview_win.win_options) do
       vim.api.nvim_set_option_value(k, v, { scope = "local", win = preview_win })
     end
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 6dc85a8..a86981a 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -668,10 +668,17 @@ M.hack_around_termopen_autocmd = function(prev_mode)
   end, 10)
 end
 
+---@param opts? {include_not_owned?: boolean}
 ---@return nil|integer
-M.get_preview_win = function()
+M.get_preview_win = function(opts)
+  opts = opts or {}
+
   for _, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
-    if vim.api.nvim_win_is_valid(winid) and vim.wo[winid].previewwindow then
+    if
+      vim.api.nvim_win_is_valid(winid)
+      and vim.wo[winid].previewwindow
+      and (opts.include_not_owned or vim.w[winid]["oil_preview"])
+    then
       return winid
     end
   end

From c5f7c56644425e2b77e71904da98cda0331b3342 Mon Sep 17 00:00:00 2001
From: David Marchante <6660202+iovis@users.noreply.github.com>
Date: Fri, 20 Dec 2024 23:18:49 -0500
Subject: [PATCH 125/206] fix: set alternate when using floating windows (#526)

---
 lua/oil/init.lua      |  2 +-
 tests/altbuf_spec.lua | 12 ++++++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 275c97d..5471bb0 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -720,7 +720,7 @@ M.select = function(opts, callback)
         vertical = opts.vertical,
         horizontal = opts.horizontal,
         split = opts.split,
-        keepalt = true,
+        keepalt = false,
       }
       local filebufnr = vim.fn.bufadd(normalized_url)
       local entry_is_file = not vim.endswith(normalized_url, "/")
diff --git a/tests/altbuf_spec.lua b/tests/altbuf_spec.lua
index 4b51604..beece15 100644
--- a/tests/altbuf_spec.lua
+++ b/tests/altbuf_spec.lua
@@ -141,5 +141,17 @@ a.describe("Alternate buffer", function()
       oil.close()
       assert.equals("foo", vim.fn.expand("#"))
     end)
+
+    a.it("preserves alternate when traversing to a new file", function()
+      vim.cmd.edit({ args = { "foo" } })
+      oil.open_float()
+      test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+      assert.equals("foo", vim.fn.expand("#"))
+      test_util.feedkeys({ "/LICENSE" }, 10)
+      oil.select()
+      test_util.wait_for_autocmd("BufEnter")
+      assert.equals("LICENSE", vim.fn.expand("%:."))
+      assert.equals("foo", vim.fn.expand("#"))
+    end)
   end)
 end)

From ba858b662599eab8ef1cba9ab745afded99cb180 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 21 Dec 2024 13:05:01 -0500
Subject: [PATCH 126/206] chore(master): release 2.14.0 (#515)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
 CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e7284a..90dfc95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,39 @@
 # Changelog
 
+## [2.14.0](https://github.com/stevearc/oil.nvim/compare/v2.13.0...v2.14.0) (2024-12-21)
+
+
+### Features
+
+* add `win_options` to `preview_win` ([#514](https://github.com/stevearc/oil.nvim/issues/514)) ([bbeed86](https://github.com/stevearc/oil.nvim/commit/bbeed86bde134da8d09bed64b6aa0d65642e6b23))
+* add highlight group for orphaned links ([#502](https://github.com/stevearc/oil.nvim/issues/502)) ([740b8fd](https://github.com/stevearc/oil.nvim/commit/740b8fd425a2b77f7f40eb5ac155ebe66ff9515c))
+* better merging of action desc when overriding keymaps ([f2b3249](https://github.com/stevearc/oil.nvim/commit/f2b324933f4d505cff6f7d445fd61fad02dcd9ae))
+* config option to customize filename highlight group ([#508](https://github.com/stevearc/oil.nvim/issues/508)) ([99ce32f](https://github.com/stevearc/oil.nvim/commit/99ce32f4a2ecf76263b72fcc31efb163faa1a941))
+* config option to disable previewing a file ([3fa3161](https://github.com/stevearc/oil.nvim/commit/3fa3161aa9515ff6a7cf7e44458b6a2114262870))
+* disable preview for large files ([#511](https://github.com/stevearc/oil.nvim/issues/511)) ([c23fe08](https://github.com/stevearc/oil.nvim/commit/c23fe08e0546d9efc242e19f0d829efa7e7b2743))
+* highlight groups for hidden files ([#459](https://github.com/stevearc/oil.nvim/issues/459)) ([60e6896](https://github.com/stevearc/oil.nvim/commit/60e68967e51ff1ecd264c29e3de0d52bfff22df3))
+* option to quite vim if oil is closed as last buffer ([#491](https://github.com/stevearc/oil.nvim/issues/491)) ([81cc9c3](https://github.com/stevearc/oil.nvim/commit/81cc9c3f62ddbef3687931d119e505643496fa0a))
+* use scratch buffer for file previews ([#467](https://github.com/stevearc/oil.nvim/issues/467)) ([21705a1](https://github.com/stevearc/oil.nvim/commit/21705a1debe6d85a53c138ab944484b685432b2b))
+
+
+### Bug Fixes
+
+* cursor sometimes does not hover previous file ([8ea40b5](https://github.com/stevearc/oil.nvim/commit/8ea40b5506115b6d355e304dd9ee5089f7d78601))
+* don't take over the preview window until it's opened for oil ([#532](https://github.com/stevearc/oil.nvim/issues/532)) ([78ab7ca](https://github.com/stevearc/oil.nvim/commit/78ab7ca1073731ebdf82efa474202defa028d5a4))
+* handle files with newlines in the name ([#534](https://github.com/stevearc/oil.nvim/issues/534)) ([dba0375](https://github.com/stevearc/oil.nvim/commit/dba037598843973b8c54bc5ce0318db4a0da439d))
+* image.nvim previews with preview_method=scratch ([5acab3d](https://github.com/stevearc/oil.nvim/commit/5acab3d8a9bc85a571688db432f2702dd7d901a4))
+* improper file name escaping ([#530](https://github.com/stevearc/oil.nvim/issues/530)) ([7a55ede](https://github.com/stevearc/oil.nvim/commit/7a55ede5e745e31ea8e4cb5483221524922294bf))
+* set alternate when using floating windows ([#526](https://github.com/stevearc/oil.nvim/issues/526)) ([c5f7c56](https://github.com/stevearc/oil.nvim/commit/c5f7c56644425e2b77e71904da98cda0331b3342))
+* work around performance issue with treesitter, folds, and large directories ([da93d55](https://github.com/stevearc/oil.nvim/commit/da93d55e32d73a17c447067d168d80290ae96590))
+
+
+### Performance Improvements
+
+* change default view_options.natural_order behavior to disable on large directories ([01b0b9d](https://github.com/stevearc/oil.nvim/commit/01b0b9d8ef79b7b631e92f6b5fed1c639262d570))
+* only sort entries after we have them all ([792f0db](https://github.com/stevearc/oil.nvim/commit/792f0db6ba8b626b14bc127e1ce7247185b3be91))
+* optimize rendering cadence ([c96f93d](https://github.com/stevearc/oil.nvim/commit/c96f93d894cc97e76b0871bec4058530eee8ece4))
+* replace vim.endswith and vim.startswith with string.match ([4de3025](https://github.com/stevearc/oil.nvim/commit/4de30256c32cd272482bc6df0c6de78ffc389153))
+
 ## [2.13.0](https://github.com/stevearc/oil.nvim/compare/v2.12.2...v2.13.0) (2024-11-11)
 
 

From 1f7da07a3ed06be418a43fb3d677143e6c1fbd7b Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Fri, 3 Jan 2025 11:48:58 -0800
Subject: [PATCH 127/206] refactor: remove overcomplicated meta_fields
 abstraction

This abstraction is overly generic for what it does. It's only ever used
to help us conditionally perform a fs_stat for the local files adapter.
We can replace that with a much dumber, much simpler bit of logic.
---
 lua/oil/adapters/files.lua | 123 +++++++++++++++++++++++--------------
 lua/oil/columns.lua        |  53 ----------------
 2 files changed, 77 insertions(+), 99 deletions(-)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 593cfce..4f757ce 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -12,6 +12,7 @@ local uv = vim.uv or vim.loop
 local M = {}
 
 local FIELD_NAME = constants.FIELD_NAME
+local FIELD_TYPE = constants.FIELD_TYPE
 local FIELD_META = constants.FIELD_META
 
 local function read_link_data(path, cb)
@@ -50,17 +51,8 @@ end
 
 local file_columns = {}
 
-local fs_stat_meta_fields = {
-  stat = function(parent_url, entry, cb)
-    local _, path = util.parse_url(parent_url)
-    assert(path)
-    local dir = fs.posix_to_os_path(path .. entry[FIELD_NAME])
-    uv.fs_stat(dir, cb)
-  end,
-}
-
 file_columns.size = {
-  meta_fields = fs_stat_meta_fields,
+  require_stat = true,
 
   render = function(entry, conf)
     local meta = entry[FIELD_META]
@@ -97,7 +89,7 @@ file_columns.size = {
 -- TODO support file permissions on windows
 if not fs.is_windows then
   file_columns.permissions = {
-    meta_fields = fs_stat_meta_fields,
+    require_stat = true,
 
     render = function(entry, conf)
       local meta = entry[FIELD_META]
@@ -160,7 +152,7 @@ end)
 
 for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
   file_columns[time_key] = {
-    meta_fields = fs_stat_meta_fields,
+    require_stat = true,
 
     render = function(entry, conf)
       local meta = entry[FIELD_META]
@@ -206,6 +198,20 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
   }
 end
 
+---@param column_defs table[]
+---@return boolean
+local function columns_require_stat(column_defs)
+  for _, def in ipairs(column_defs) do
+    local name = util.split_config(def)
+    local column = M.get_column(name)
+    ---@diagnostic disable-next-line: undefined-field We only put this on the files adapter columns
+    if column and column.require_stat then
+      return true
+    end
+  end
+  return false
+end
+
 ---@param name string
 ---@return nil|oil.ColumnDefinition
 M.get_column = function(name)
@@ -283,12 +289,64 @@ M.get_entry_path = function(url, entry, cb)
   end
 end
 
+---@param parent_dir string
+---@param entry oil.InternalEntry
+---@param require_stat boolean
+---@param cb fun(err?: string)
+local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
+  local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME])
+  local meta = entry[FIELD_META]
+  if not meta then
+    meta = {}
+    entry[FIELD_META] = meta
+  end
+
+  -- Make sure we always get fs_stat info for links
+  if entry[FIELD_TYPE] == "link" then
+    read_link_data(entry_path, function(link_err, link, link_stat)
+      if link_err then
+        return cb(link_err)
+      end
+      meta.link = link
+      if link_stat then
+        -- Use the fstat of the linked file as the stat for the link
+        meta.link_stat = link_stat
+        meta.stat = link_stat
+      elseif require_stat then
+        -- The link is broken, so let's use the stat of the link itself
+        uv.fs_lstat(entry_path, function(stat_err, stat)
+          if stat_err then
+            return cb(stat_err)
+          end
+          meta.stat = stat
+          cb()
+        end)
+        return
+      end
+
+      cb()
+    end)
+  elseif require_stat then
+    uv.fs_stat(entry_path, function(stat_err, stat)
+      if stat_err then
+        return cb(stat_err)
+      end
+      assert(stat)
+      meta.stat = stat
+      cb()
+    end)
+  else
+    cb()
+  end
+end
+
 ---@param url string
 ---@param column_defs string[]
 ---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
 local function list_windows_drives(url, column_defs, cb)
-  ---@cast M oil.FilesAdapter
-  local fetch_meta = columns.get_metadata_fetcher(M, column_defs)
+  local _, path = util.parse_url(url)
+  assert(path)
+  local require_stat = columns_require_stat(column_defs)
   local stdout = ""
   local jid = vim.fn.jobstart({ "wmic", "logicaldisk", "get", "name" }, {
     stdout_buffered = true,
@@ -318,14 +376,8 @@ local function list_windows_drives(url, column_defs, cb)
         else
           disk = disk:gsub(":%s*$", "")
           local cache_entry = cache.create_entry(url, disk, "directory")
-          fetch_meta(url, cache_entry, function(err)
-            if err then
-              complete_disk_cb(err)
-            else
-              table.insert(internal_entries, cache_entry)
-              complete_disk_cb()
-            end
-          end)
+          table.insert(internal_entries, cache_entry)
+          fetch_entry_metadata(path, cache_entry, require_stat, complete_disk_cb)
         end
       end
     end,
@@ -345,8 +397,7 @@ M.list = function(url, column_defs, cb)
     return list_windows_drives(url, column_defs, cb)
   end
   local dir = fs.posix_to_os_path(path)
-  ---@cast M oil.Adapter
-  local fetch_meta = columns.get_metadata_fetcher(M, column_defs)
+  local require_stat = columns_require_stat(column_defs)
 
   ---@diagnostic disable-next-line: param-type-mismatch, discard-returns
   uv.fs_opendir(dir, function(open_err, fd)
@@ -378,28 +429,8 @@ M.list = function(url, column_defs, cb)
           end)
           for _, entry in ipairs(entries) do
             local cache_entry = cache.create_entry(url, entry.name, entry.type)
-            fetch_meta(url, cache_entry, function(meta_err)
-              table.insert(internal_entries, cache_entry)
-              local meta = cache_entry[FIELD_META]
-              -- Make sure we always get fs_stat info for links
-              if entry.type == "link" then
-                read_link_data(fs.join(dir, entry.name), function(link_err, link, link_stat)
-                  if link_err then
-                    poll(link_err)
-                  else
-                    if not meta then
-                      meta = {}
-                      cache_entry[FIELD_META] = meta
-                    end
-                    meta.link = link
-                    meta.link_stat = link_stat
-                    poll()
-                  end
-                end)
-              else
-                poll()
-              end
-            end)
+            table.insert(internal_entries, cache_entry)
+            fetch_entry_metadata(path, cache_entry, require_stat, poll)
           end
         else
           uv.fs_closedir(fd, function(close_err)
diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua
index d882792..106c853 100644
--- a/lua/oil/columns.lua
+++ b/lua/oil/columns.lua
@@ -14,7 +14,6 @@ local all_columns = {}
 ---@class (exact) oil.ColumnDefinition
 ---@field render fun(entry: oil.InternalEntry, conf: nil|table): nil|oil.TextChunk
 ---@field parse fun(line: string, conf: nil|table): nil|string, nil|string
----@field meta_fields? table
 ---@field compare? fun(entry: oil.InternalEntry, parsed_value: any): boolean
 ---@field render_action? fun(action: oil.ChangeAction): string
 ---@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string))
@@ -54,46 +53,6 @@ M.get_supported_columns = function(adapter_or_scheme)
   return ret
 end
 
----@param adapter oil.Adapter
----@param column_defs table[]
----@return fun(parent_url: string, entry: oil.InternalEntry, cb: fun(err: nil|string))
-M.get_metadata_fetcher = function(adapter, column_defs)
-  local keyfetches = {}
-  local num_keys = 0
-  for _, def in ipairs(column_defs) do
-    local name = util.split_config(def)
-    local column = M.get_column(adapter, name)
-    if column and column.meta_fields then
-      for k, v in pairs(column.meta_fields) do
-        if not keyfetches[k] then
-          keyfetches[k] = v
-          num_keys = num_keys + 1
-        end
-      end
-    end
-  end
-  if num_keys == 0 then
-    return function(_, _, cb)
-      cb()
-    end
-  end
-  return function(parent_url, entry, cb)
-    cb = util.cb_collect(num_keys, cb)
-    local meta = {}
-    entry[FIELD_META] = meta
-    for k, v in pairs(keyfetches) do
-      v(parent_url, entry, function(err, value)
-        if err then
-          cb(err)
-        else
-          meta[k] = value
-          cb()
-        end
-      end)
-    end
-  end
-end
-
 local EMPTY = { "-", "Comment" }
 
 M.EMPTY = EMPTY
@@ -110,18 +69,6 @@ M.render_col = function(adapter, col_def, entry)
     return EMPTY
   end
 
-  -- Make sure all the required metadata exists before attempting to render
-  if column.meta_fields then
-    local meta = entry[FIELD_META]
-    if not meta then
-      return EMPTY
-    end
-    for k in pairs(column.meta_fields) do
-      if not meta[k] then
-        return EMPTY
-      end
-    end
-  end
   local chunk = column.render(entry, conf)
   if type(chunk) == "table" then
     if chunk[1]:match("^%s*$") then

From c6a39a69b2df7c10466f150dde0bd23e49c1fba3 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Fri, 3 Jan 2025 11:49:20 -0800
Subject: [PATCH 128/206] fix: stat files if fs_readdir doesn't provide a type
 (#543)

---
 lua/oil/adapters/files.lua | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 4f757ce..e89e8ef 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -301,6 +301,12 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
     entry[FIELD_META] = meta
   end
 
+  -- Sometimes fs_readdir entries don't have a type, so we need to stat them.
+  -- See https://github.com/stevearc/oil.nvim/issues/543
+  if not require_stat and not entry[FIELD_TYPE] then
+    require_stat = true
+  end
+
   -- Make sure we always get fs_stat info for links
   if entry[FIELD_TYPE] == "link" then
     read_link_data(entry_path, function(link_err, link, link_stat)
@@ -332,6 +338,7 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
         return cb(stat_err)
       end
       assert(stat)
+      entry[FIELD_TYPE] = stat.type
       meta.stat = stat
       cb()
     end)

From 254bc6635cb3f77e6e9a89155652f368e5535160 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sat, 4 Jan 2025 12:52:26 -0800
Subject: [PATCH 129/206] fix: guard against nil metadata values (#548)

---
 lua/oil/adapters/files.lua             | 12 ++++++------
 lua/oil/adapters/ssh.lua               |  4 ++--
 lua/oil/adapters/trash/freedesktop.lua | 14 +++++++-------
 lua/oil/adapters/trash/windows.lua     | 16 ++++++++--------
 4 files changed, 23 insertions(+), 23 deletions(-)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index e89e8ef..9206a5d 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -56,7 +56,7 @@ file_columns.size = {
 
   render = function(entry, conf)
     local meta = entry[FIELD_META]
-    local stat = meta.stat
+    local stat = meta and meta.stat
     if not stat then
       return columns.EMPTY
     end
@@ -73,7 +73,7 @@ file_columns.size = {
 
   get_sort_value = function(entry)
     local meta = entry[FIELD_META]
-    local stat = meta.stat
+    local stat = meta and meta.stat
     if stat then
       return stat.size
     else
@@ -93,7 +93,7 @@ if not fs.is_windows then
 
     render = function(entry, conf)
       local meta = entry[FIELD_META]
-      local stat = meta.stat
+      local stat = meta and meta.stat
       if not stat then
         return columns.EMPTY
       end
@@ -106,7 +106,7 @@ if not fs.is_windows then
 
     compare = function(entry, parsed_value)
       local meta = entry[FIELD_META]
-      if parsed_value and meta.stat and meta.stat.mode then
+      if parsed_value and meta and meta.stat and meta.stat.mode then
         local mask = bit.lshift(1, 12) - 1
         local old_mode = bit.band(meta.stat.mode, mask)
         if parsed_value ~= old_mode then
@@ -156,7 +156,7 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
 
     render = function(entry, conf)
       local meta = entry[FIELD_META]
-      local stat = meta.stat
+      local stat = meta and meta.stat
       if not stat then
         return columns.EMPTY
       end
@@ -188,7 +188,7 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
 
     get_sort_value = function(entry)
       local meta = entry[FIELD_META]
-      local stat = meta.stat
+      local stat = meta and meta.stat
       if stat then
         return stat[time_key].sec
       else
diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua
index 0b619ae..02637a8 100644
--- a/lua/oil/adapters/ssh.lua
+++ b/lua/oil/adapters/ssh.lua
@@ -126,7 +126,7 @@ ssh_columns.permissions = {
 
   compare = function(entry, parsed_value)
     local meta = entry[FIELD_META]
-    if parsed_value and meta.mode then
+    if parsed_value and meta and meta.mode then
       local mask = bit.lshift(1, 12) - 1
       local old_mode = bit.band(meta.mode, mask)
       if parsed_value ~= old_mode then
@@ -169,7 +169,7 @@ ssh_columns.size = {
 
   get_sort_value = function(entry)
     local meta = entry[FIELD_META]
-    if meta.size then
+    if meta and meta.size then
       return meta.size
     else
       return 0
diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua
index ac45492..07e8a7f 100644
--- a/lua/oil/adapters/trash/freedesktop.lua
+++ b/lua/oil/adapters/trash/freedesktop.lua
@@ -131,7 +131,7 @@ end
 ---@param cb fun(path: string)
 M.get_entry_path = function(url, entry, cb)
   local internal_entry = assert(cache.get_entry_by_id(entry.id))
-  local meta = internal_entry[FIELD_META]
+  local meta = assert(internal_entry[FIELD_META])
   ---@type oil.TrashInfo
   local trash_info = meta.trash_info
   if not trash_info then
@@ -381,7 +381,7 @@ file_columns.mtime = {
   get_sort_value = function(entry)
     local meta = entry[FIELD_META]
     ---@type nil|oil.TrashInfo
-    local trash_info = meta.trash_info
+    local trash_info = meta and meta.trash_info
     if trash_info then
       return trash_info.deletion_date
     else
@@ -417,7 +417,7 @@ M.filter_action = function(action)
   elseif action.type == "delete" then
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
-    return meta.trash_info ~= nil
+    return meta ~= nil and meta.trash_info ~= nil
   elseif action.type == "move" then
     local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
     local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
@@ -447,7 +447,7 @@ M.render_action = function(action)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.TrashInfo
-    local trash_info = meta.trash_info
+    local trash_info = meta and meta.trash_info
     local short_path = fs.shorten_path(trash_info.original_path)
     return string.format(" PURGE %s", short_path)
   elseif action.type == "move" then
@@ -561,7 +561,7 @@ M.perform_action = function(action, cb)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.TrashInfo
-    local trash_info = meta.trash_info
+    local trash_info = meta and meta.trash_info
     purge(trash_info, cb)
   elseif action.type == "move" then
     local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
@@ -576,7 +576,7 @@ M.perform_action = function(action, cb)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META]
       ---@type oil.TrashInfo
-      local trash_info = meta.trash_info
+      local trash_info = meta and meta.trash_info
       fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err)
         if err then
           return cb(err)
@@ -608,7 +608,7 @@ M.perform_action = function(action, cb)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META]
       ---@type oil.TrashInfo
-      local trash_info = meta.trash_info
+      local trash_info = meta and meta.trash_info
       fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
     else
       error("Must be moving files into or out of trash")
diff --git a/lua/oil/adapters/trash/windows.lua b/lua/oil/adapters/trash/windows.lua
index ecfca61..4605567 100644
--- a/lua/oil/adapters/trash/windows.lua
+++ b/lua/oil/adapters/trash/windows.lua
@@ -164,8 +164,8 @@ file_columns.mtime = {
 
   get_sort_value = function(entry)
     local meta = entry[FIELD_META]
-    ---@type oil.WindowsTrashInfo
-    local trash_info = meta.trash_info
+    ---@type nil|oil.WindowsTrashInfo
+    local trash_info = meta and meta.trash_info
     if trash_info and trash_info.deletion_date then
       return trash_info.deletion_date
     else
@@ -199,7 +199,7 @@ M.filter_action = function(action)
   elseif action.type == "delete" then
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
-    return meta.trash_info ~= nil
+    return meta ~= nil and meta.trash_info ~= nil
   elseif action.type == "move" then
     local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
     local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
@@ -235,7 +235,7 @@ end
 M.get_entry_path = function(url, entry, cb)
   local internal_entry = assert(cache.get_entry_by_id(entry.id))
   local meta = internal_entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
-  local trash_info = meta.trash_info
+  local trash_info = meta and meta.trash_info
   if not trash_info then
     -- This is a subpath in the trash
     M.normalize_url(url, cb)
@@ -265,7 +265,7 @@ M.render_action = function(action)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.WindowsTrashInfo
-    local trash_info = meta.trash_info
+    local trash_info = meta and meta.trash_info
     local short_path = fs.shorten_path(trash_info.original_path)
     return string.format(" PURGE %s", short_path)
   elseif action.type == "move" then
@@ -348,7 +348,7 @@ M.perform_action = function(action, cb)
   if action.type == "delete" then
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
-    local trash_info = meta.trash_info
+    local trash_info = meta and meta.trash_info
 
     purge(trash_info, cb)
   elseif action.type == "move" then
@@ -364,7 +364,7 @@ M.perform_action = function(action, cb)
       dest_path = fs.posix_to_os_path(dest_path)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
-      local trash_info = meta.trash_info
+      local trash_info = meta and meta.trash_info
       fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err)
         if err then
           return cb(err)
@@ -388,7 +388,7 @@ M.perform_action = function(action, cb)
       dest_path = fs.posix_to_os_path(dest_path)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META] --[[@as {stat: uv_fs_t, trash_info: oil.WindowsTrashInfo, display_name: string}]]
-      local trash_info = meta.trash_info
+      local trash_info = meta and meta.trash_info
       fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
     else
       error("Must be moving files into or out of trash")

From c12fad2d225d8f81fadd48521d253607fe25465c Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sat, 4 Jan 2025 21:50:16 -0800
Subject: [PATCH 130/206] doc: update winbar recipe to be window-specific
 (#546)

---
 doc/recipes.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/doc/recipes.md b/doc/recipes.md
index f32fd6e..0a19598 100644
--- a/doc/recipes.md
+++ b/doc/recipes.md
@@ -36,7 +36,8 @@ require("oil").setup({
 ```lua
 -- Declare a global function to retrieve the current directory
 function _G.get_oil_winbar()
-  local dir = require("oil").get_current_dir()
+  local bufnr = vim.api.nvim_win_get_buf(vim.g.statusline_winid)
+  local dir = require("oil").get_current_dir(bufnr)
   if dir then
     return vim.fn.fnamemodify(dir, ":~")
   else

From b082ad5eb9367b68fb766587df449387c6999c4f Mon Sep 17 00:00:00 2001
From: abdennourzahaf 
Date: Tue, 7 Jan 2025 06:05:03 +0100
Subject: [PATCH 131/206] test: update test script shebang to be compatible
 with NixOS (#550)

---
 run_tests.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/run_tests.sh b/run_tests.sh
index 98b4fa7..3018bc0 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 set -e
 
 mkdir -p ".testenv/config/nvim"

From a6a4f48b14b4a51fded531c86f6c04b4503a2ef8 Mon Sep 17 00:00:00 2001
From: Ian Wright <49083526+Landaman@users.noreply.github.com>
Date: Tue, 7 Jan 2025 00:11:10 -0500
Subject: [PATCH 132/206] fix: directory rendering with custom highlights
 (#551)

These would loose their trailing '/', making them unusable
---
 lua/oil/view.lua | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index c6d9603..1facdcb 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -770,6 +770,10 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
     else
       local hl = get_custom_hl(external_entry, is_hidden, false, false)
       if hl then
+        -- Add the trailing / if this is a directory, this is important
+        if entry_type == "directory" then
+          name = name .. "/"
+        end
         table.insert(cols, { name, hl })
         return cols
       end

From f5c563a074a38cee5a09f98e98b74dcd2c322490 Mon Sep 17 00:00:00 2001
From: Ian Wright <49083526+Landaman@users.noreply.github.com>
Date: Tue, 7 Jan 2025 21:56:03 -0500
Subject: [PATCH 133/206] feat: pass oil bufnr to custom filename highlight
 function (#552)

This enables you to determine the full directory path, enabling e.g.,
HL groups for Git
---
 README.md          |  3 ++-
 doc/oil.txt        |  2 +-
 lua/oil/config.lua |  2 +-
 lua/oil/view.lua   | 13 +++++++------
 4 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md
index 0b8d0e5..661a708 100644
--- a/README.md
+++ b/README.md
@@ -233,7 +233,8 @@ require("oil").setup({
       { "name", "asc" },
     },
     -- Customize the highlight group for the file name
-    highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
+    highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan,
+      bufnr)
       return nil
     end,
   },
diff --git a/doc/oil.txt b/doc/oil.txt
index 8540cbd..6f84dd1 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -118,7 +118,7 @@ CONFIG                                                                *oil-confi
           { "name", "asc" },
         },
         -- Customize the highlight group for the file name
-        highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
+        highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan, bufnr)
           return nil
         end,
       },
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 9102727..faace1c 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -286,7 +286,7 @@ local M = {}
 ---@field natural_order boolean|"fast"
 ---@field case_insensitive boolean
 ---@field sort oil.SortSpec[]
----@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean): string|nil
+---@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean, bufnr: integer): string|nil
 
 ---@class (exact) oil.SetupViewOptions
 ---@field show_hidden? boolean Show files and directories that start with "."
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 1facdcb..e860975 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -644,14 +644,14 @@ local function render_buffer(bufnr, opts)
 
   if M.should_display("..", bufnr) then
     local cols =
-      M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter, true)
+      M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter, true, bufnr)
     table.insert(line_table, cols)
   end
 
   for _, entry in ipairs(entry_list) do
     local should_display, is_hidden = M.should_display(entry[FIELD_NAME], bufnr)
     if should_display then
-      local cols = M.format_entry_cols(entry, column_defs, col_width, adapter, is_hidden)
+      local cols = M.format_entry_cols(entry, column_defs, col_width, adapter, is_hidden, bufnr)
       table.insert(line_table, cols)
 
       local name = entry[FIELD_NAME]
@@ -723,8 +723,9 @@ end
 ---@param col_width integer[]
 ---@param adapter oil.Adapter
 ---@param is_hidden boolean
+---@param bufnr integer
 ---@return oil.TextChunk[]
-M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden)
+M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden, bufnr)
   local name = entry[FIELD_NAME]
   local meta = entry[FIELD_META]
   local hl_suffix = ""
@@ -760,15 +761,15 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
     if entry_type == "link" then
       link_name, link_target = get_link_text(name, meta)
       local is_orphan = not (meta and meta.link_stat)
-      link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan)
+      link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan, bufnr)
 
       if link_target then
-        link_target_hl = get_custom_hl(external_entry, is_hidden, true, is_orphan)
+        link_target_hl = get_custom_hl(external_entry, is_hidden, true, is_orphan, bufnr)
       end
 
       -- intentional fallthrough
     else
-      local hl = get_custom_hl(external_entry, is_hidden, false, false)
+      local hl = get_custom_hl(external_entry, is_hidden, false, false, bufnr)
       if hl then
         -- Add the trailing / if this is a directory, this is important
         if entry_type == "directory" then

From 6290ba1dc2882883d3081978da482676eb885395 Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Wed, 8 Jan 2025 02:56:23 +0000
Subject: [PATCH 134/206] [docgen] Update docs skip-checks: true

---
 README.md   | 3 +--
 doc/oil.txt | 2 +-
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 661a708..0b8d0e5 100644
--- a/README.md
+++ b/README.md
@@ -233,8 +233,7 @@ require("oil").setup({
       { "name", "asc" },
     },
     -- Customize the highlight group for the file name
-    highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan,
-      bufnr)
+    highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
       return nil
     end,
   },
diff --git a/doc/oil.txt b/doc/oil.txt
index 6f84dd1..8540cbd 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -118,7 +118,7 @@ CONFIG                                                                *oil-confi
           { "name", "asc" },
         },
         -- Customize the highlight group for the file name
-        highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan, bufnr)
+        highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan)
           return nil
         end,
       },

From 1df90faf927e78f5aacf278abd0bfdcb5f45e825 Mon Sep 17 00:00:00 2001
From: abdennourzahaf 
Date: Wed, 8 Jan 2025 04:07:22 +0100
Subject: [PATCH 135/206] feat: floating window max width/height can be
 percentages (#553)

---
 README.md          | 1 +
 doc/oil.txt        | 1 +
 lua/oil/config.lua | 1 +
 lua/oil/layout.lua | 6 ++++--
 4 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 0b8d0e5..5975181 100644
--- a/README.md
+++ b/README.md
@@ -256,6 +256,7 @@ require("oil").setup({
   float = {
     -- Padding around the floating window
     padding = 2,
+    -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
     max_width = 0,
     max_height = 0,
     border = "rounded",
diff --git a/doc/oil.txt b/doc/oil.txt
index 8540cbd..8df959c 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -141,6 +141,7 @@ CONFIG                                                                *oil-confi
       float = {
         -- Padding around the floating window
         padding = 2,
+        -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
         max_width = 0,
         max_height = 0,
         border = "rounded",
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index faace1c..3ed851e 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -124,6 +124,7 @@ local default_config = {
   float = {
     -- Padding around the floating window
     padding = 2,
+    -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
     max_width = 0,
     max_height = 0,
     border = "rounded",
diff --git a/lua/oil/layout.lua b/lua/oil/layout.lua
index 8ed7b4e..6d4563d 100644
--- a/lua/oil/layout.lua
+++ b/lua/oil/layout.lua
@@ -115,11 +115,13 @@ M.get_fullscreen_win_opts = function()
     width = width - 2 -- The border consumes 1 col on each side
   end
   if config.float.max_width > 0 then
-    width = math.min(width, config.float.max_width)
+    local max_width = math.floor(calc_float(config.float.max_width, total_width))
+    width = math.min(width, max_width)
   end
   local height = total_height - 2 * config.float.padding
   if config.float.max_height > 0 then
-    height = math.min(height, config.float.max_height)
+    local max_height = math.floor(calc_float(config.float.max_height, total_height))
+    height = math.min(height, max_height)
   end
   local row = math.floor((total_height - height) / 2)
   local col = math.floor((total_width - width) / 2) - 1 -- adjust for border width

From 7041528bdedb350ad66e650684deec8456e053cc Mon Sep 17 00:00:00 2001
From: Benedict Ozua <62128128+BozeBro@users.noreply.github.com>
Date: Wed, 8 Jan 2025 00:04:25 -0500
Subject: [PATCH 136/206] fix: support permissions checks on windows and
 virtual filesystems (#555)

* use access(2) over file permission checks to workaround systems that change expected file permission view

* cleanup: delete unused function

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 lua/oil/adapters/files.lua | 36 ++----------------------------------
 1 file changed, 2 insertions(+), 34 deletions(-)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 9206a5d..9cd38ed 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -455,26 +455,6 @@ M.list = function(url, column_defs, cb)
   end, 10000)
 end
 
----@type nil|integer[]
-local _group_ids
----@return integer[]
-local function get_group_ids()
-  if not _group_ids then
-    local output = vim.fn.system({ "id", "-G" })
-    if vim.v.shell_error == 0 then
-      _group_ids = vim.tbl_map(tonumber, vim.split(output, "%s+", { trimempty = true }))
-    else
-      -- If the id command fails, fall back to just using the process group
-      _group_ids = { uv.getgid() }
-      vim.notify(
-        "[oil] missing the `id` command. Some directories may not be modifiable even if you have group access.",
-        vim.log.levels.WARN
-      )
-    end
-  end
-  return _group_ids
-end
-
 ---@param bufnr integer
 ---@return boolean
 M.is_modifiable = function(bufnr)
@@ -490,20 +470,8 @@ M.is_modifiable = function(bufnr)
     return true
   end
 
-  -- Can't do permissions checks on windows
-  if fs.is_windows then
-    return true
-  end
-
-  local uid = uv.getuid()
-  local rwx = stat.mode
-  if uid == stat.uid then
-    rwx = bit.bor(rwx, bit.rshift(stat.mode, 6))
-  end
-  if vim.tbl_contains(get_group_ids(), stat.gid) then
-    rwx = bit.bor(rwx, bit.rshift(stat.mode, 3))
-  end
-  return bit.band(rwx, 2) ~= 0
+  -- fs_access can return nil, force boolean return
+  return uv.fs_access(dir, "W") == true
 end
 
 ---@param action oil.Action

From 7c26a59ac0061b199bf9f44b19d45cfadd9b14f5 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
Date: Sun, 12 Jan 2025 14:29:46 -0800
Subject: [PATCH 137/206] fix: gracefully handle fs_stat failures (#558)

* fix: gracefully handle fs_stat failures

* fix: make log methods safe to call in luv callbacks

* fix: replace another vimscript call
---
 lua/oil/adapters/files.lua |  10 ++-
 lua/oil/log.lua            | 126 +++++++++++++++++++++++++++++++++++++
 2 files changed, 133 insertions(+), 3 deletions(-)
 create mode 100644 lua/oil/log.lua

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 9cd38ed..ac56c4e 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -4,6 +4,7 @@ local config = require("oil.config")
 local constants = require("oil.constants")
 local fs = require("oil.fs")
 local git = require("oil.git")
+local log = require("oil.log")
 local permissions = require("oil.adapters.files.permissions")
 local trash = require("oil.adapters.files.trash")
 local util = require("oil.util")
@@ -311,7 +312,8 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
   if entry[FIELD_TYPE] == "link" then
     read_link_data(entry_path, function(link_err, link, link_stat)
       if link_err then
-        return cb(link_err)
+        log.warn("Error reading link data %s: %s", entry_path, link_err)
+        return cb()
       end
       meta.link = link
       if link_stat then
@@ -322,7 +324,8 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
         -- The link is broken, so let's use the stat of the link itself
         uv.fs_lstat(entry_path, function(stat_err, stat)
           if stat_err then
-            return cb(stat_err)
+            log.warn("Error lstat link file %s: %s", entry_path, stat_err)
+            return cb()
           end
           meta.stat = stat
           cb()
@@ -335,7 +338,8 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
   elseif require_stat then
     uv.fs_stat(entry_path, function(stat_err, stat)
       if stat_err then
-        return cb(stat_err)
+        log.warn("Error stat file %s: %s", entry_path, stat_err)
+        return cb()
       end
       assert(stat)
       entry[FIELD_TYPE] = stat.type
diff --git a/lua/oil/log.lua b/lua/oil/log.lua
new file mode 100644
index 0000000..28a4f9c
--- /dev/null
+++ b/lua/oil/log.lua
@@ -0,0 +1,126 @@
+local uv = vim.uv or vim.loop
+local levels_reverse = {}
+for k, v in pairs(vim.log.levels) do
+  levels_reverse[v] = k
+end
+
+local Log = {}
+
+---@type integer
+Log.level = vim.log.levels.WARN
+
+---@return string
+Log.get_logfile = function()
+  local fs = require("oil.fs")
+
+  local ok, stdpath = pcall(vim.fn.stdpath, "log")
+  if not ok then
+    stdpath = vim.fn.stdpath("cache")
+  end
+  assert(type(stdpath) == "string")
+  return fs.join(stdpath, "oil.log")
+end
+
+---@param level integer
+---@param msg string
+---@param ... any[]
+---@return string
+local function format(level, msg, ...)
+  local args = vim.F.pack_len(...)
+  for i = 1, args.n do
+    local v = args[i]
+    if type(v) == "table" then
+      args[i] = vim.inspect(v)
+    elseif v == nil then
+      args[i] = "nil"
+    end
+  end
+  local ok, text = pcall(string.format, msg, vim.F.unpack_len(args))
+  -- TODO figure out how to get formatted time inside luv callback
+  -- local timestr = vim.fn.strftime("%Y-%m-%d %H:%M:%S")
+  local timestr = ""
+  if ok then
+    local str_level = levels_reverse[level]
+    return string.format("%s[%s] %s", timestr, str_level, text)
+  else
+    return string.format(
+      "%s[ERROR] error formatting log line: '%s' args %s",
+      timestr,
+      vim.inspect(msg),
+      vim.inspect(args)
+    )
+  end
+end
+
+---@param line string
+local function write(line)
+  -- This will be replaced during initialization
+end
+
+local initialized = false
+local function initialize()
+  if initialized then
+    return
+  end
+  initialized = true
+  local filepath = Log.get_logfile()
+
+  local stat = uv.fs_stat(filepath)
+  if stat and stat.size > 10 * 1024 * 1024 then
+    local backup = filepath .. ".1"
+    uv.fs_unlink(backup)
+    uv.fs_rename(filepath, backup)
+  end
+
+  local parent = vim.fs.dirname(filepath)
+  require("oil.fs").mkdirp(parent)
+
+  local logfile, openerr = io.open(filepath, "a+")
+  if not logfile then
+    local err_msg = string.format("Failed to open oil.nvim log file: %s", openerr)
+    vim.notify(err_msg, vim.log.levels.ERROR)
+  else
+    write = function(line)
+      logfile:write(line)
+      logfile:write("\n")
+      logfile:flush()
+    end
+  end
+end
+
+---Override the file handler e.g. for tests
+---@param handler fun(line: string)
+function Log.set_handler(handler)
+  write = handler
+  initialized = true
+end
+
+function Log.log(level, msg, ...)
+  if Log.level <= level then
+    initialize()
+    local text = format(level, msg, ...)
+    write(text)
+  end
+end
+
+function Log.trace(...)
+  Log.log(vim.log.levels.TRACE, ...)
+end
+
+function Log.debug(...)
+  Log.log(vim.log.levels.DEBUG, ...)
+end
+
+function Log.info(...)
+  Log.log(vim.log.levels.INFO, ...)
+end
+
+function Log.warn(...)
+  Log.log(vim.log.levels.WARN, ...)
+end
+
+function Log.error(...)
+  Log.log(vim.log.levels.ERROR, ...)
+end
+
+return Log

From 09fa1d22f5edf0730824d2b222d726c8c81bbdc9 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
Date: Mon, 13 Jan 2025 10:22:59 -0800
Subject: [PATCH 138/206] fix: work around incorrect link detection on windows
 (#557)

* fix: work around incorrect link detection on windows

* fix: gracefully handle lstat error on windows
---
 lua/oil/adapters/files.lua | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index ac56c4e..b7f5363 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -351,6 +351,34 @@ local function fetch_entry_metadata(parent_dir, entry, require_stat, cb)
   end
 end
 
+-- On windows, sometimes the entry type from fs_readdir is "link" but the actual type is not.
+-- See https://github.com/stevearc/oil.nvim/issues/535
+if fs.is_windows then
+  local old_fetch_metadata = fetch_entry_metadata
+  fetch_entry_metadata = function(parent_dir, entry, require_stat, cb)
+    if entry[FIELD_TYPE] == "link" then
+      local entry_path = fs.posix_to_os_path(parent_dir .. entry[FIELD_NAME])
+      uv.fs_lstat(entry_path, function(stat_err, stat)
+        if stat_err then
+          log.warn("Error lstat link file %s: %s", entry_path, stat_err)
+          return old_fetch_metadata(parent_dir, entry, require_stat, cb)
+        end
+        assert(stat)
+        entry[FIELD_TYPE] = stat.type
+        local meta = entry[FIELD_META]
+        if not meta then
+          meta = {}
+          entry[FIELD_META] = meta
+        end
+        meta.stat = stat
+        old_fetch_metadata(parent_dir, entry, require_stat, cb)
+      end)
+    else
+      return old_fetch_metadata(parent_dir, entry, require_stat, cb)
+    end
+  end
+end
+
 ---@param url string
 ---@param column_defs string[]
 ---@param cb fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())

From 8d11a2abf3039b1974d4acd65fbc83ada2ca1084 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 22 Jan 2025 08:26:28 -0800
Subject: [PATCH 139/206] fix: error when non-current oil buffer has validation
 errors (#561)

---
 lua/oil/mutator/init.lua | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua
index f15c069..9bff041 100644
--- a/lua/oil/mutator/init.lua
+++ b/lua/oil/mutator/init.lua
@@ -553,10 +553,13 @@ M.try_write_changes = function(confirm, cb)
         { all_errors[curbuf][1].lnum + 1, all_errors[curbuf][1].col }
       )
     else
-      ---@diagnostic disable-next-line: param-type-mismatch
-      local bufnr, errs = next(pairs(all_errors))
-      vim.api.nvim_win_set_buf(0, bufnr)
-      pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col })
+      local bufnr, errs = next(all_errors)
+      -- HACK: This is a workaround for the fact that we can't switch buffers in the middle of a
+      -- BufWriteCmd.
+      vim.schedule(function()
+        vim.api.nvim_win_set_buf(0, bufnr)
+        pcall(vim.api.nvim_win_set_cursor, 0, { errs[1].lnum + 1, errs[1].col })
+      end)
     end
     unlock()
     cb("Error parsing oil buffers")

From 62c5683c2e4f968dce27048e11e72f662d6d90e5 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 22 Jan 2025 08:54:47 -0800
Subject: [PATCH 140/206] lint: fix typecheck errors

---
 lua/oil/init.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 5471bb0..f5eff2f 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -7,7 +7,7 @@ local M = {}
 ---@field parsed_name nil|string
 ---@field meta nil|table
 
----@alias oil.EntryType "file"|"directory"|"socket"|"link"|"fifo"
+---@alias oil.EntryType uv.aliases.fs_types
 ---@alias oil.HlRange { [1]: string, [2]: integer, [3]: integer } A tuple of highlight group name, col_start, col_end
 ---@alias oil.HlTuple { [1]: string, [2]: string } A tuple of text, highlight group
 ---@alias oil.HlRangeTuple { [1]: string, [2]: oil.HlRange[] } A tuple of text, internal highlights

From c80fa5c415b882c1c694a32748cea09b7dafc2c5 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 22 Jan 2025 15:10:41 -0800
Subject: [PATCH 141/206] fix: more consistent cursor position when entering a
 new directory (#536)

---
 lua/oil/view.lua | 43 +++++++++++++++++++++++--------------------
 1 file changed, 23 insertions(+), 20 deletions(-)

diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index e860975..dd66842 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -258,8 +258,9 @@ local function get_first_mutable_column_col(adapter, ranges)
 end
 
 ---Force cursor to be after hidden/immutable columns
-local function constrain_cursor()
-  if not config.constrain_cursor then
+---@param mode false|"name"|"editable"
+local function constrain_cursor(mode)
+  if not mode then
     return
   end
   local parser = require("oil.mutator.parser")
@@ -275,14 +276,12 @@ local function constrain_cursor()
   local result = parser.parse_line(adapter, line, column_defs)
   if result and result.ranges then
     local min_col
-    if config.constrain_cursor == "editable" then
+    if mode == "editable" then
       min_col = get_first_mutable_column_col(adapter, result.ranges)
-    elseif config.constrain_cursor == "name" then
+    elseif mode == "name" then
       min_col = result.ranges.name[1]
     else
-      error(
-        string.format('Unexpected value "%s" for option constrain_cursor', config.constrain_cursor)
-      )
+      error(string.format('Unexpected value "%s" for option constrain_cursor', mode))
     end
     if cur[2] < min_col then
       vim.api.nvim_win_set_cursor(0, { cur[1], min_col })
@@ -407,7 +406,7 @@ M.initialize = function(bufnr)
     callback = function()
       -- For some reason the cursor bounces back to its original position,
       -- so we have to defer the call
-      vim.schedule(constrain_cursor)
+      vim.schedule_wrap(constrain_cursor)(config.constrain_cursor)
     end,
   })
   vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
@@ -420,7 +419,7 @@ M.initialize = function(bufnr)
         return
       end
 
-      constrain_cursor()
+      constrain_cursor(config.constrain_cursor)
 
       if config.preview_win.update_on_cursor_moved then
         -- Debounce and update the preview window
@@ -675,19 +674,23 @@ local function render_buffer(bufnr, opts)
     vim.schedule(function()
       for _, winid in ipairs(vim.api.nvim_list_wins()) do
         if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then
-          -- If we're not jumping to a specific lnum, use the current lnum so we can adjust the col
-          local lnum = jump_idx or vim.api.nvim_win_get_cursor(winid)[1]
-          local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
-          local id_str = line:match("^/(%d+)")
-          local id = tonumber(id_str)
-          if id then
-            local entry = cache.get_entry_by_id(id)
-            if entry then
-              local name = entry[FIELD_NAME]
-              local col = line:find(name, 1, true) or (id_str:len() + 1)
-              vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 })
+          if jump_idx then
+            local lnum = jump_idx
+            local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
+            local id_str = line:match("^/(%d+)")
+            local id = tonumber(id_str)
+            if id then
+              local entry = cache.get_entry_by_id(id)
+              if entry then
+                local name = entry[FIELD_NAME]
+                local col = line:find(name, 1, true) or (id_str:len() + 1)
+                vim.api.nvim_win_set_cursor(winid, { lnum, col - 1 })
+                return
+              end
             end
           end
+
+          constrain_cursor("name")
         end
       end
     end)

From 1488f0d96b1cb820dd12f05a7bf5283a631a7c4d Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 22 Jan 2025 16:53:10 -0800
Subject: [PATCH 142/206] fix: preview sometimes causes oil buffers to be stuck
 in unloaded state (#563)

---
 lua/oil/init.lua | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index f5eff2f..812b414 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -30,6 +30,8 @@ local M = {}
 ---@field filter_action? fun(action: oil.Action): boolean When present, filter out actions as they are created
 ---@field filter_error? fun(action: oil.ParseError): boolean When present, filter out errors from parsing a buffer
 
+local load_oil_buffer
+
 ---Get the entry on a specific line (1-indexed)
 ---@param bufnr integer
 ---@param lnum integer
@@ -568,6 +570,12 @@ M.open_preview = function(opts, callback)
       vim.api.nvim_echo({ { err, "Error" } }, true, {})
     end
 
+    -- If we called open_preview during an autocmd, then the edit command may not trigger the
+    -- BufReadCmd to load the buffer. So we need to do it manually.
+    if util.is_oil_bufnr(filebufnr) then
+      load_oil_buffer(filebufnr)
+    end
+
     vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 })
     vim.api.nvim_win_set_var(0, "oil_preview", true)
     for k, v in pairs(config.preview_win.win_options) do
@@ -994,7 +1002,7 @@ local function restore_alt_buf()
 end
 
 ---@param bufnr integer
-local function load_oil_buffer(bufnr)
+load_oil_buffer = function(bufnr)
   local config = require("oil.config")
   local keymap_util = require("oil.keymap_util")
   local loading = require("oil.loading")
@@ -1008,6 +1016,11 @@ local function load_oil_buffer(bufnr)
     util.rename_buffer(bufnr, bufname)
   end
 
+  -- Early return if we're already loading or have already loaded this buffer
+  if loading.is_loading(bufnr) or vim.b[bufnr].filetype ~= nil then
+    return
+  end
+
   local adapter = assert(config.get_adapter_by_scheme(scheme))
 
   if vim.endswith(bufname, "/") then

From 8615e7da2032f6a53ca0918bfd0ea9069cccac9d Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 22 Jan 2025 21:17:21 -0800
Subject: [PATCH 143/206] cleanup: remove open({preview = true}) shim

---
 lua/oil/init.lua | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 812b414..0677067 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -612,14 +612,6 @@ M.select = function(opts, callback)
   local FIELD_META = constants.FIELD_META
   opts = vim.tbl_extend("keep", opts or {}, {})
 
-  if opts.preview then
-    vim.notify_once(
-      "Deprecated: do not call oil.select with preview=true. Use oil.open_preview instead.\nThis shim will be removed on 2025-01-01"
-    )
-    M.open_preview(opts, callback)
-    return
-  end
-
   local function finish(err)
     if err then
       vim.notify(err, vim.log.levels.ERROR)

From 1b180d5491a225617eb56223f5934237292e1fc3 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 22 Jan 2025 21:18:35 -0800
Subject: [PATCH 144/206] doc: rephrase the instructions to restore a trashed
 file

---
 doc/oil.txt         | 6 +++---
 scripts/generate.py | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index 8df959c..1dd27d2 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -698,9 +698,9 @@ of being permanently deleted. You can browse the trash for a directory using
 the `toggle_trash` action (bound to `g\` by default). You can view all files
 in the trash with `:Oil --trash /`.
 
-To restore files, simply delete them from the trash and put them in the desired
-destination, the same as any other file operation. If you delete files from the
-trash they will be permanently deleted (purged).
+To restore files, simply move them from the trash to the desired destination,
+the same as any other file operation. If you delete files from the trash they
+will be permanently deleted (purged).
 
 Linux:
     Oil supports the FreeDesktop trash specification.
diff --git a/scripts/generate.py b/scripts/generate.py
index a20ad53..fac256d 100755
--- a/scripts/generate.py
+++ b/scripts/generate.py
@@ -351,9 +351,9 @@ of being permanently deleted. You can browse the trash for a directory using
 the `toggle_trash` action (bound to `g\\` by default). You can view all files
 in the trash with `:Oil --trash /`.
 
-To restore files, simply delete them from the trash and put them in the desired
-destination, the same as any other file operation. If you delete files from the
-trash they will be permanently deleted (purged).
+To restore files, simply move them from the trash to the desired destination,
+the same as any other file operation. If you delete files from the trash they
+will be permanently deleted (purged).
 
 Linux:
     Oil supports the FreeDesktop trash specification.

From 7a782c9a9cb7a16ec52199e55a1e892262c5dbbc Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 22 Jan 2025 21:19:02 -0800
Subject: [PATCH 145/206] refactor: officially deprecated trash_command

---
 lua/oil/config.lua | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 3ed851e..562562c 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -394,6 +394,13 @@ local M = {}
 M.setup = function(opts)
   opts = opts or {}
 
+  if opts.trash_command then
+    vim.notify(
+      "[oil.nvim] trash_command is deprecated. Use built-in trash functionality instead (:help oil-trash).\nCompatibility will be removed on 2025-06-01.",
+      vim.log.levels.WARN
+    )
+  end
+
   local new_conf = vim.tbl_deep_extend("keep", opts, default_config)
   if not new_conf.use_default_keymaps then
     new_conf.keymaps = opts.keymaps or {}

From 83ac5185f79ab8d869bccea792dc516ad02ad06e Mon Sep 17 00:00:00 2001
From: Peeranut Pongpakatien 
Date: Fri, 24 Jan 2025 13:08:06 +0700
Subject: [PATCH 146/206] fix: open files in correct window from floating oil
 (#560)

---
 lua/oil/init.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 0677067..46fd69d 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -709,7 +709,7 @@ M.select = function(opts, callback)
     else
       -- Close floating window before opening a file
       if vim.w.is_oil_win then
-        vim.api.nvim_win_close(0, false)
+        M.close()
       end
     end
 

From 52f1683c7664819508e1d2fc85051d4a20c5d643 Mon Sep 17 00:00:00 2001
From: Julian Visser <12615757+justmejulian@users.noreply.github.com>
Date: Fri, 24 Jan 2025 19:51:18 +0100
Subject: [PATCH 147/206] doc: add note discouraging lazy loading (#565)

* Add disable lazy loading to lazy.nvim install

* doc: rephrase

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 README.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 5975181..87a825d 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,9 @@ oil.nvim supports all the usual plugin managers
   opts = {},
   -- Optional dependencies
   dependencies = { { "echasnovski/mini.icons", opts = {} } },
-  -- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if prefer nvim-web-devicons
+  -- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if you prefer nvim-web-devicons
+  -- Lazy loading is not recommended because it is very tricky to make it work correctly in all situations.
+  lazy = false,
 }
 ```
 

From 57528bf9c58080ca891e8d362d0a578895c136ce Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Fri, 24 Jan 2025 15:09:36 -0800
Subject: [PATCH 148/206] feat: API to automatically open preview window after
 opening oil (#339)

---
 README.md              |  4 ++--
 doc/api.md             | 36 +++++++++++++++++++----------
 doc/oil.txt            | 28 ++++++++++++++++++-----
 lua/oil/init.lua       | 51 +++++++++++++++++++++++++++++++++---------
 tests/preview_spec.lua | 42 ++++++++++++++++++++++++++++++++++
 tests/test_util.lua    |  5 +++++
 6 files changed, 135 insertions(+), 31 deletions(-)
 create mode 100644 tests/preview_spec.lua

diff --git a/README.md b/README.md
index 87a825d..ec3355b 100644
--- a/README.md
+++ b/README.md
@@ -372,9 +372,9 @@ Note that at the moment the ssh adapter does not support Windows machines, and i
 - [set_is_hidden_file(is_hidden_file)](doc/api.md#set_is_hidden_fileis_hidden_file)
 - [toggle_hidden()](doc/api.md#toggle_hidden)
 - [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr)
-- [open_float(dir)](doc/api.md#open_floatdir)
+- [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb)
 - [toggle_float(dir)](doc/api.md#toggle_floatdir)
-- [open(dir)](doc/api.md#opendir)
+- [open(dir, opts, cb)](doc/api.md#opendir-opts-cb)
 - [close(opts)](doc/api.md#closeopts)
 - [open_preview(opts, callback)](doc/api.md#open_previewopts-callback)
 - [select(opts, callback)](doc/api.md#selectopts-callback)
diff --git a/doc/api.md b/doc/api.md
index 2f51cdd..3e6a3e6 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -10,9 +10,9 @@
 - [set_is_hidden_file(is_hidden_file)](#set_is_hidden_fileis_hidden_file)
 - [toggle_hidden()](#toggle_hidden)
 - [get_current_dir(bufnr)](#get_current_dirbufnr)
-- [open_float(dir)](#open_floatdir)
+- [open_float(dir, opts, cb)](#open_floatdir-opts-cb)
 - [toggle_float(dir)](#toggle_floatdir)
-- [open(dir)](#opendir)
+- [open(dir, opts, cb)](#opendir-opts-cb)
 - [close(opts)](#closeopts)
 - [open_preview(opts, callback)](#open_previewopts-callback)
 - [select(opts, callback)](#selectopts-callback)
@@ -92,14 +92,20 @@ Get the current directory
 | ----- | -------------- | ---- |
 | bufnr | `nil\|integer` |      |
 
-## open_float(dir)
+## open_float(dir, opts, cb)
 
-`open_float(dir)` \
+`open_float(dir, opts, cb)` \
 Open oil browser in a floating window
 
-| Param | Type          | Desc                                                                                        |
-| ----- | ------------- | ------------------------------------------------------------------------------------------- |
-| dir   | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
+| Param        | Type                                                    | Desc                                                                                        |
+| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| dir          | `nil\|string`                                           | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
+| opts         | `nil\|oil.OpenOpts`                                     |                                                                                             |
+| >preview     | `nil\|oil.OpenPreviewOpts`                              | When present, open the preview window after opening oil                                     |
+| >>vertical   | `nil\|boolean`                                          | Open the buffer in a vertical split                                                         |
+| >>horizontal | `nil\|boolean`                                          | Open the buffer in a horizontal split                                                       |
+| >>split      | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier                                                                              |
+| cb           | `nil\|fun()`                                            | Called after the oil buffer is ready                                                        |
 
 ## toggle_float(dir)
 
@@ -110,14 +116,20 @@ Open oil browser in a floating window, or close it if open
 | ----- | ------------- | ------------------------------------------------------------------------------------------- |
 | dir   | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
 
-## open(dir)
+## open(dir, opts, cb)
 
-`open(dir)` \
+`open(dir, opts, cb)` \
 Open oil browser for a directory
 
-| Param | Type          | Desc                                                                                        |
-| ----- | ------------- | ------------------------------------------------------------------------------------------- |
-| dir   | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
+| Param        | Type                                                    | Desc                                                                                        |
+| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| dir          | `nil\|string`                                           | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
+| opts         | `nil\|oil.OpenOpts`                                     |                                                                                             |
+| >preview     | `nil\|oil.OpenPreviewOpts`                              | When present, open the preview window after opening oil                                     |
+| >>vertical   | `nil\|boolean`                                          | Open the buffer in a vertical split                                                         |
+| >>horizontal | `nil\|boolean`                                          | Open the buffer in a horizontal split                                                       |
+| >>split      | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier                                                                              |
+| cb           | `nil\|fun()`                                            | Called after the oil buffer is ready                                                        |
 
 ## close(opts)
 
diff --git a/doc/oil.txt b/doc/oil.txt
index 1dd27d2..df8754b 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -304,12 +304,20 @@ get_current_dir({bufnr}): nil|string                         *oil.get_current_di
     Parameters:
       {bufnr} `nil|integer`
 
-open_float({dir})                                                 *oil.open_float*
+open_float({dir}, {opts}, {cb})                                   *oil.open_float*
     Open oil browser in a floating window
 
     Parameters:
-      {dir} `nil|string` When nil, open the parent of the current buffer, or the
-            cwd if current buffer is not a file
+      {dir}  `nil|string` When nil, open the parent of the current buffer, or
+             the cwd if current buffer is not a file
+      {opts} `nil|oil.OpenOpts`
+          {preview} `nil|oil.OpenPreviewOpts` When present, open the preview
+                    window after opening oil
+              {vertical}   `nil|boolean` Open the buffer in a vertical split
+              {horizontal} `nil|boolean` Open the buffer in a horizontal split
+              {split}      `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S
+                           plit modifier
+      {cb}   `nil|fun()` Called after the oil buffer is ready
 
 toggle_float({dir})                                             *oil.toggle_float*
     Open oil browser in a floating window, or close it if open
@@ -318,12 +326,20 @@ toggle_float({dir})                                             *oil.toggle_floa
       {dir} `nil|string` When nil, open the parent of the current buffer, or the
             cwd if current buffer is not a file
 
-open({dir})                                                             *oil.open*
+open({dir}, {opts}, {cb})                                               *oil.open*
     Open oil browser for a directory
 
     Parameters:
-      {dir} `nil|string` When nil, open the parent of the current buffer, or the
-            cwd if current buffer is not a file
+      {dir}  `nil|string` When nil, open the parent of the current buffer, or
+             the cwd if current buffer is not a file
+      {opts} `nil|oil.OpenOpts`
+          {preview} `nil|oil.OpenPreviewOpts` When present, open the preview
+                    window after opening oil
+              {vertical}   `nil|boolean` Open the buffer in a vertical split
+              {horizontal} `nil|boolean` Open the buffer in a horizontal split
+              {split}      `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S
+                           plit modifier
+      {cb}   `nil|fun()` Called after the oil buffer is ready
 
 close({opts})                                                          *oil.close*
     Restore the buffer that was present when oil was opened
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 46fd69d..16a3dc9 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -241,18 +241,21 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent)
   end
 end
 
+---@class (exact) oil.OpenOpts
+---@field preview? oil.OpenPreviewOpts When present, open the preview window after opening oil
+
 ---Open oil browser in a floating window
----@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
-M.open_float = function(dir)
+---@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
+---@param opts? oil.OpenOpts
+---@param cb? fun() Called after the oil buffer is ready
+M.open_float = function(dir, opts, cb)
+  opts = opts or {}
   local config = require("oil.config")
   local layout = require("oil.layout")
   local util = require("oil.util")
   local view = require("oil.view")
 
   local parent_url, basename = M.get_url_for_path(dir)
-  if not parent_url then
-    return
-  end
   if basename then
     view.set_last_cursor(parent_url, basename)
   end
@@ -326,6 +329,14 @@ M.open_float = function(dir)
     vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 })
   end
 
+  util.run_after_load(0, function()
+    if opts.preview then
+      M.open_preview(opts.preview, cb)
+    elseif cb then
+      cb()
+    end
+  end)
+
   if vim.fn.has("nvim-0.9") == 0 then
     util.add_title_to_win(winid)
   end
@@ -359,15 +370,15 @@ local function update_preview_window(oil_bufnr)
 end
 
 ---Open oil browser for a directory
----@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
-M.open = function(dir)
+---@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
+---@param opts? oil.OpenOpts
+---@param cb? fun() Called after the oil buffer is ready
+M.open = function(dir, opts, cb)
+  opts = opts or {}
   local config = require("oil.config")
   local util = require("oil.util")
   local view = require("oil.view")
   local parent_url, basename = M.get_url_for_path(dir)
-  if not parent_url then
-    return
-  end
   if basename then
     view.set_last_cursor(parent_url, basename)
   end
@@ -377,6 +388,14 @@ M.open = function(dir)
     vim.api.nvim_set_option_value("buflisted", config.buf_options.buflisted, { buf = 0 })
   end
 
+  util.run_after_load(0, function()
+    if opts.preview then
+      M.open_preview(opts.preview, cb)
+    elseif cb then
+      cb()
+    end
+  end)
+
   -- If preview window exists, update its content
   update_preview_window()
 end
@@ -1104,6 +1123,7 @@ M.setup = function(opts)
     end
     local float = false
     local trash = false
+    local preview = false
     local i = 1
     while i <= #args.fargs do
       local v = args.fargs[i]
@@ -1113,6 +1133,11 @@ M.setup = function(opts)
       elseif v == "--trash" then
         trash = true
         table.remove(args.fargs, i)
+      elseif v == "--preview" then
+        -- In the future we may want to support specifying options for the preview window (e.g.
+        -- vertical/horizontal), but if you want that level of control maybe just use the API
+        preview = true
+        table.remove(args.fargs, i)
       elseif v == "--progress" then
         local mutator = require("oil.mutator")
         if mutator.is_mutating() then
@@ -1136,12 +1161,16 @@ M.setup = function(opts)
 
     local method = float and "open_float" or "open"
     local path = args.fargs[1]
+    local opts = {}
     if trash then
       local url = M.get_url_for_path(path, false)
       local _, new_path = util.parse_url(url)
       path = "oil-trash://" .. new_path
     end
-    M[method](path)
+    if preview then
+      opts.preview = {}
+    end
+    M[method](path, opts)
   end, { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir" })
   local aug = vim.api.nvim_create_augroup("Oil", {})
 
diff --git a/tests/preview_spec.lua b/tests/preview_spec.lua
new file mode 100644
index 0000000..b386001
--- /dev/null
+++ b/tests/preview_spec.lua
@@ -0,0 +1,42 @@
+require("plenary.async").tests.add_to_env()
+local TmpDir = require("tests.tmpdir")
+local oil = require("oil")
+local test_util = require("tests.test_util")
+local util = require("oil.util")
+
+a.describe("oil preview", function()
+  local tmpdir
+  a.before_each(function()
+    tmpdir = TmpDir.new()
+  end)
+  a.after_each(function()
+    if tmpdir then
+      tmpdir:dispose()
+    end
+    test_util.reset_editor()
+  end)
+
+  a.it("opens preview window", function()
+    tmpdir:create({ "a.txt" })
+    oil.open(tmpdir.path)
+    test_util.wait_oil_ready()
+    a.wrap(oil.open_preview, 2)()
+    local preview_win = util.get_preview_win()
+    assert.not_nil(preview_win)
+    assert(preview_win)
+    local bufnr = vim.api.nvim_win_get_buf(preview_win)
+    local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
+    assert.are.same({ "a.txt" }, preview_lines)
+  end)
+
+  a.it("opens preview window when open(preview={})", function()
+    tmpdir:create({ "a.txt" })
+    a.wrap(oil.open, 3)(tmpdir.path, { preview = {} })
+    local preview_win = util.get_preview_win()
+    assert.not_nil(preview_win)
+    assert(preview_win)
+    local bufnr = vim.api.nvim_win_get_buf(preview_win)
+    local preview_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
+    assert.are.same({ "a.txt" }, preview_lines)
+  end)
+end)
diff --git a/tests/test_util.lua b/tests/test_util.lua
index 0076689..bb09524 100644
--- a/tests/test_util.lua
+++ b/tests/test_util.lua
@@ -1,6 +1,7 @@
 require("plenary.async").tests.add_to_env()
 local cache = require("oil.cache")
 local test_adapter = require("oil.adapters.test")
+local util = require("oil.util")
 local M = {}
 
 M.reset_editor = function()
@@ -53,6 +54,10 @@ M.wait_for_autocmd = a.wrap(function(autocmd, cb)
   vim.api.nvim_create_autocmd(autocmd, opts)
 end, 2)
 
+M.wait_oil_ready = a.wrap(function(cb)
+  util.run_after_load(0, vim.schedule_wrap(cb))
+end, 1)
+
 ---@param actions string[]
 ---@param timestep integer
 M.feedkeys = function(actions, timestep)

From 2f6ed7016105b2cc8e0cd65090765ef4b99a1f8e Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Fri, 24 Jan 2025 15:41:27 -0800
Subject: [PATCH 149/206] test: refactor tests to use new helper methods

---
 tests/files_spec.lua       |  4 ++--
 tests/preview_spec.lua     |  5 ++---
 tests/select_spec.lua      | 24 ++++++++----------------
 tests/test_util.lua        |  4 ++++
 tests/win_options_spec.lua | 18 ++++++------------
 5 files changed, 22 insertions(+), 33 deletions(-)

diff --git a/tests/files_spec.lua b/tests/files_spec.lua
index b268b4c..66a70d0 100644
--- a/tests/files_spec.lua
+++ b/tests/files_spec.lua
@@ -150,10 +150,10 @@ a.describe("files adapter", function()
   a.it("Editing a new oil://path/ creates an oil buffer", function()
     local tmpdir_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "/"
     vim.cmd.edit({ args = { tmpdir_url } })
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.wait_oil_ready()
     local new_url = "oil://" .. vim.fn.fnamemodify(tmpdir.path, ":p") .. "newdir"
     vim.cmd.edit({ args = { new_url } })
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.wait_oil_ready()
     assert.equals("oil", vim.bo.filetype)
     -- The normalization will add a '/'
     assert.equals(new_url .. "/", vim.api.nvim_buf_get_name(0))
diff --git a/tests/preview_spec.lua b/tests/preview_spec.lua
index b386001..08aba78 100644
--- a/tests/preview_spec.lua
+++ b/tests/preview_spec.lua
@@ -18,8 +18,7 @@ a.describe("oil preview", function()
 
   a.it("opens preview window", function()
     tmpdir:create({ "a.txt" })
-    oil.open(tmpdir.path)
-    test_util.wait_oil_ready()
+    test_util.oil_open(tmpdir.path)
     a.wrap(oil.open_preview, 2)()
     local preview_win = util.get_preview_win()
     assert.not_nil(preview_win)
@@ -31,7 +30,7 @@ a.describe("oil preview", function()
 
   a.it("opens preview window when open(preview={})", function()
     tmpdir:create({ "a.txt" })
-    a.wrap(oil.open, 3)(tmpdir.path, { preview = {} })
+    test_util.oil_open(tmpdir.path, { preview = {} })
     local preview_win = util.get_preview_win()
     assert.not_nil(preview_win)
     assert(preview_win)
diff --git a/tests/select_spec.lua b/tests/select_spec.lua
index d4d3700..9de2eb4 100644
--- a/tests/select_spec.lua
+++ b/tests/select_spec.lua
@@ -8,8 +8,7 @@ a.describe("oil select", function()
   end)
 
   a.it("opens file under cursor", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     -- Go to the bottom, so the cursor is not on a directory
     vim.cmd.normal({ args = { "G" } })
     a.wrap(oil.select, 2)()
@@ -18,8 +17,7 @@ a.describe("oil select", function()
   end)
 
   a.it("opens file in new tab", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     local tabpage = vim.api.nvim_get_current_tabpage()
     a.wrap(oil.select, 2)({ tab = true })
     assert.equals(2, #vim.api.nvim_list_tabpages())
@@ -28,8 +26,7 @@ a.describe("oil select", function()
   end)
 
   a.it("opens file in new split", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     local winid = vim.api.nvim_get_current_win()
     a.wrap(oil.select, 2)({ vertical = true })
     assert.equals(1, #vim.api.nvim_list_tabpages())
@@ -38,8 +35,7 @@ a.describe("oil select", function()
   end)
 
   a.it("opens multiple files in new tabs", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     vim.api.nvim_feedkeys("Vj", "x", true)
     local tabpage = vim.api.nvim_get_current_tabpage()
     a.wrap(oil.select, 2)({ tab = true })
@@ -49,8 +45,7 @@ a.describe("oil select", function()
   end)
 
   a.it("opens multiple files in new splits", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     vim.api.nvim_feedkeys("Vj", "x", true)
     local winid = vim.api.nvim_get_current_win()
     a.wrap(oil.select, 2)({ vertical = true })
@@ -63,8 +58,7 @@ a.describe("oil select", function()
     a.it("same window", function()
       vim.cmd.edit({ args = { "foo" } })
       local bufnr = vim.api.nvim_get_current_buf()
-      oil.open()
-      test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+      test_util.oil_open()
       -- Go to the bottom, so the cursor is not on a directory
       vim.cmd.normal({ args = { "G" } })
       a.wrap(oil.select, 2)({ close = true })
@@ -79,8 +73,7 @@ a.describe("oil select", function()
       vim.cmd.edit({ args = { "foo" } })
       local bufnr = vim.api.nvim_get_current_buf()
       local winid = vim.api.nvim_get_current_win()
-      oil.open()
-      test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+      test_util.oil_open()
       a.wrap(oil.select, 2)({ vertical = true, close = true })
       assert.equals(2, #vim.api.nvim_tabpage_list_wins(0))
       assert.equals(bufnr, vim.api.nvim_win_get_buf(winid))
@@ -90,8 +83,7 @@ a.describe("oil select", function()
       vim.cmd.edit({ args = { "foo" } })
       local bufnr = vim.api.nvim_get_current_buf()
       local tabpage = vim.api.nvim_get_current_tabpage()
-      oil.open()
-      test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+      test_util.oil_open()
       a.wrap(oil.select, 2)({ tab = true, close = true })
       assert.equals(1, #vim.api.nvim_tabpage_list_wins(0))
       assert.equals(2, #vim.api.nvim_list_tabpages())
diff --git a/tests/test_util.lua b/tests/test_util.lua
index bb09524..46f63df 100644
--- a/tests/test_util.lua
+++ b/tests/test_util.lua
@@ -34,6 +34,10 @@ local function throwiferr(err, ...)
   end
 end
 
+M.oil_open = function(...)
+  a.wrap(require("oil").open, 3)(...)
+end
+
 M.await = function(fn, nargs, ...)
   return throwiferr(a.wrap(fn, nargs)(...))
 end
diff --git a/tests/win_options_spec.lua b/tests/win_options_spec.lua
index 6dbde27..cb638a7 100644
--- a/tests/win_options_spec.lua
+++ b/tests/win_options_spec.lua
@@ -9,32 +9,28 @@ a.describe("window options", function()
 
   a.it("Restores window options on close", function()
     vim.cmd.edit({ args = { "README.md" } })
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     assert.equals("no", vim.o.signcolumn)
     oil.close()
     assert.equals("auto", vim.o.signcolumn)
   end)
 
   a.it("Restores window options on edit", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.edit({ args = { "README.md" } })
     assert.equals("auto", vim.o.signcolumn)
   end)
 
   a.it("Restores window options on split ", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.split({ args = { "README.md" } })
     assert.equals("auto", vim.o.signcolumn)
   end)
 
   a.it("Restores window options on split", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.split()
     vim.cmd.edit({ args = { "README.md" } })
@@ -42,16 +38,14 @@ a.describe("window options", function()
   end)
 
   a.it("Restores window options on tabnew ", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.tabnew({ args = { "README.md" } })
     assert.equals("auto", vim.o.signcolumn)
   end)
 
   a.it("Restores window options on tabnew", function()
-    oil.open()
-    test_util.wait_for_autocmd({ "User", pattern = "OilEnter" })
+    test_util.oil_open()
     assert.equals("no", vim.o.signcolumn)
     vim.cmd.tabnew()
     vim.cmd.edit({ args = { "README.md" } })

From 6f9e1057c589b7f63d06b51d6094ebd66d904561 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Fri, 24 Jan 2025 16:00:44 -0800
Subject: [PATCH 150/206] lint: rename shadowed variable

---
 lua/oil/init.lua | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 16a3dc9..e11400d 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -1161,16 +1161,16 @@ M.setup = function(opts)
 
     local method = float and "open_float" or "open"
     local path = args.fargs[1]
-    local opts = {}
+    local open_opts = {}
     if trash then
       local url = M.get_url_for_path(path, false)
       local _, new_path = util.parse_url(url)
       path = "oil-trash://" .. new_path
     end
     if preview then
-      opts.preview = {}
+      open_opts.preview = {}
     end
-    M[method](path, opts)
+    M[method](path, open_opts)
   end, { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir" })
   local aug = vim.api.nvim_create_augroup("Oil", {})
 

From 81b2c5f04ae24a8c83b20ecbd017fecac15faca0 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Fri, 24 Jan 2025 16:22:21 -0800
Subject: [PATCH 151/206] fix: crash in preview on nvim 0.8

---
 lua/oil/util.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index a86981a..b5e57e5 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -926,7 +926,7 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
     return
   end
   local ft = vim.filetype.match({ filename = path, buf = bufnr })
-  if ft and ft ~= "" then
+  if ft and ft ~= "" and vim.treesitter.language.get_lang then
     local lang = vim.treesitter.language.get_lang(ft)
     if not pcall(vim.treesitter.start, bufnr, lang) then
       vim.bo[bufnr].syntax = ft

From a3fc6623fa9a3c49ed94b5dbe9f181fbd2e93e64 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Fri, 24 Jan 2025 22:54:40 -0800
Subject: [PATCH 152/206] lint: upgrade to stylua v2.0.2

---
 .github/workflows/tests.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 08957e5..6a4fd73 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -34,7 +34,7 @@ jobs:
         uses: JohnnyMorganz/stylua-action@v4
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
-          version: v0.20.0
+          version: v2.0.2
           args: --check lua tests
 
   typecheck:

From b594b9a9052618669ccf6520b2d0c0d942eb8118 Mon Sep 17 00:00:00 2001
From: Anton Janshagen <68065682+Janshagen@users.noreply.github.com>
Date: Sun, 26 Jan 2025 18:18:37 +0100
Subject: [PATCH 153/206] feat: can selectively add entries to quickfix (#564)

* bugfix: fix to enable adding or replacing of quickfix entries

* feat: added option to send only matched files to the quickfix list
---
 doc/oil.txt         |  9 ++++++---
 lua/oil/actions.lua |  6 ++++++
 lua/oil/util.lua    | 25 ++++++++++++++++++++-----
 3 files changed, 32 insertions(+), 8 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index df8754b..2f232cf 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -605,9 +605,12 @@ send_to_qflist                                            *actions.send_to_qflis
     previous entries.
 
     Parameters:
-      {action} `"r"|"a"` Replace or add to current quickfix list (see
-               |setqflist-action|)
-      {target} `"qflist"|"loclist"` The target list to send files to
+      {target}               `"qflist"|"loclist"` The target list to send files to
+      {action}               `"r"|"a"` Replace or add to current quickfix list
+                             (see |setqflist-action|)
+      {only_matching_search} `boolean` Whether to only add the files that matches
+                             the last search. This option only applies when search
+                             highlighting is active
 
 show_help                                                      *actions.show_help*
     Show default keymaps
diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua
index 8dabe40..cef37c7 100644
--- a/lua/oil/actions.lua
+++ b/lua/oil/actions.lua
@@ -504,10 +504,12 @@ M.send_to_qflist = {
     opts = vim.tbl_deep_extend("keep", opts or {}, {
       target = "qflist",
       action = "r",
+      only_matching_search = false,
     })
     util.send_to_quickfix({
       target = opts.target,
       action = opts.action,
+      only_matching_search = opts.only_matching_search,
     })
   end,
   parameters = {
@@ -519,6 +521,10 @@ M.send_to_qflist = {
       type = '"r"|"a"',
       desc = "Replace or add to current quickfix list (see |setqflist-action|)",
     },
+    only_matching_search = {
+      type = "boolean",
+      desc = "Whether to only add the files that matches the last search. This option only applies when search highlighting is active",
+    },
   },
 }
 
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index b5e57e5..707aa24 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -753,7 +753,7 @@ end
 
 ---Send files from the current oil directory to quickfix
 ---based on the provided options.
----@param opts {target?: "qflist"|"loclist", mode?: "r"|"a"}
+---@param opts {target?: "qflist"|"loclist", action?: "r"|"a", only_matching_search?: boolean}
 M.send_to_quickfix = function(opts)
   if type(opts) ~= "table" then
     opts = {}
@@ -767,10 +767,11 @@ M.send_to_quickfix = function(opts)
   if not range then
     range = { start_lnum = 1, end_lnum = vim.fn.line("$") }
   end
+  local match_all = not opts.only_matching_search
   local qf_entries = {}
   for i = range.start_lnum, range.end_lnum do
     local entry = oil.get_entry_on_line(0, i)
-    if entry and entry.type == "file" then
+    if entry and entry.type == "file" and (match_all or M.is_matching(entry)) then
       local qf_entry = {
         filename = dir .. entry.name,
         lnum = 1,
@@ -786,13 +787,14 @@ M.send_to_quickfix = function(opts)
   end
   vim.api.nvim_exec_autocmds("QuickFixCmdPre", {})
   local qf_title = "oil files"
-  local mode = opts.mode == "a" and "a" or "r"
+  local action = opts.action == "a" and "a" or "r"
   if opts.target == "loclist" then
-    vim.fn.setloclist(0, {}, mode, { title = qf_title, items = qf_entries })
+    vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries })
   else
-    vim.fn.setqflist({}, mode, { title = qf_title, items = qf_entries })
+    vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries })
   end
   vim.api.nvim_exec_autocmds("QuickFixCmdPost", {})
+  vim.cmd.copen()
 end
 
 ---@return boolean
@@ -817,6 +819,19 @@ M.get_visual_range = function()
   return { start_lnum = start_lnum, end_lnum = end_lnum }
 end
 
+---@param entry oil.Entry
+---@return boolean
+M.is_matching = function(entry)
+  -- if search highlightig is not enabled, all files are considered to match
+  local search_highlighting_is_off = (vim.v.hlsearch == 0)
+  if search_highlighting_is_off then
+    return true
+  end
+  local pattern = vim.fn.getreg("/")
+  local position_of_match = vim.fn.match(entry.name, pattern)
+  return position_of_match ~= -1
+end
+
 ---@param bufnr integer
 ---@param callback fun()
 M.run_after_load = function(bufnr, callback)

From add50252b5e9147c0a09d36480d418c7e2737472 Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Sun, 26 Jan 2025 17:18:56 +0000
Subject: [PATCH 154/206] [docgen] Update docs skip-checks: true

---
 doc/oil.txt | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index 2f232cf..fea9bfd 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -605,12 +605,12 @@ send_to_qflist                                            *actions.send_to_qflis
     previous entries.
 
     Parameters:
-      {target}               `"qflist"|"loclist"` The target list to send files to
-      {action}               `"r"|"a"` Replace or add to current quickfix list
-                             (see |setqflist-action|)
-      {only_matching_search} `boolean` Whether to only add the files that matches
-                             the last search. This option only applies when search
-                             highlighting is active
+      {action} `"r"|"a"` Replace or add to current quickfix list (see
+               |setqflist-action|)
+      {only_matching_search} `boolean` Whether to only add the files that
+               matches the last search. This option only applies when search
+               highlighting is active
+      {target} `"qflist"|"loclist"` The target list to send files to
 
 show_help                                                      *actions.show_help*
     Show default keymaps

From 20baf827476a14d09df1783ca4f0a8d012d5c597 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Tue, 11 Feb 2025 17:42:01 -0800
Subject: [PATCH 155/206] ci: update nvim install script for new appimage name

---
 .github/workflows/install_nvim.sh | 12 ++++++++----
 .github/workflows/tests.yml       |  1 +
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/install_nvim.sh b/.github/workflows/install_nvim.sh
index 4c0203c..9ba2d26 100644
--- a/.github/workflows/install_nvim.sh
+++ b/.github/workflows/install_nvim.sh
@@ -1,12 +1,16 @@
 #!/bin/bash
 set -e
-PLUGINS="$HOME/.local/share/nvim/site/pack/plugins/start"
-mkdir -p "$PLUGINS"
-
-wget "https://github.com/neovim/neovim/releases/download/${NVIM_TAG-stable}/nvim.appimage"
+version="${NVIM_TAG-stable}"
+dl_name="nvim-linux-x86_64.appimage"
+# The appimage name changed in v0.10.4
+if python -c 'from packaging.version import Version; import sys; sys.exit(not (Version(sys.argv[1]) < Version("v0.10.4")))' "$version" 2>/dev/null; then
+  dl_name="nvim.appimage"
+fi
+curl -sL "https://github.com/neovim/neovim/releases/download/${version}/${dl_name}" -o nvim.appimage
 chmod +x nvim.appimage
 ./nvim.appimage --appimage-extract >/dev/null
 rm -f nvim.appimage
 mkdir -p ~/.local/share/nvim
 mv squashfs-root ~/.local/share/nvim/appimage
 sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim
+/usr/bin/nvim --version
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 6a4fd73..bdcc4c3 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -53,6 +53,7 @@ jobs:
           - nvim_tag: v0.8.3
           - nvim_tag: v0.9.4
           - nvim_tag: v0.10.0
+          - nvim_tag: v0.10.4
 
     name: Run tests
     runs-on: ubuntu-22.04

From abbfbd0dbcaa78c3dcdada191ea23e50a41e5806 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Tue, 11 Feb 2025 21:05:46 -0800
Subject: [PATCH 156/206] lint: fix typecheck warning

---
 lua/oil/mutator/init.lua | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua
index 9bff041..cd0043f 100644
--- a/lua/oil/mutator/init.lua
+++ b/lua/oil/mutator/init.lua
@@ -554,6 +554,8 @@ M.try_write_changes = function(confirm, cb)
       )
     else
       local bufnr, errs = next(all_errors)
+      assert(bufnr)
+      assert(errs)
       -- HACK: This is a workaround for the fact that we can't switch buffers in the middle of a
       -- BufWriteCmd.
       vim.schedule(function()

From 8abc58b038f84078121ab1cac6ecad0163fe1635 Mon Sep 17 00:00:00 2001
From: Ian Wright <49083526+Landaman@users.noreply.github.com>
Date: Wed, 12 Feb 2025 19:49:43 -0500
Subject: [PATCH 157/206] feat: add support for bufnr in column rendering
 functions (#575)

This is primarily for user-defined custom columns, which may want access
to the current path or similar information
---
 lua/oil/columns.lua | 7 ++++---
 lua/oil/view.lua    | 2 +-
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua
index 106c853..cc3b445 100644
--- a/lua/oil/columns.lua
+++ b/lua/oil/columns.lua
@@ -12,7 +12,7 @@ local all_columns = {}
 ---@alias oil.ColumnSpec string|{[1]: string, [string]: any}
 
 ---@class (exact) oil.ColumnDefinition
----@field render fun(entry: oil.InternalEntry, conf: nil|table): nil|oil.TextChunk
+---@field render fun(entry: oil.InternalEntry, conf: nil|table, bufnr: integer): nil|oil.TextChunk
 ---@field parse fun(line: string, conf: nil|table): nil|string, nil|string
 ---@field compare? fun(entry: oil.InternalEntry, parsed_value: any): boolean
 ---@field render_action? fun(action: oil.ChangeAction): string
@@ -60,8 +60,9 @@ M.EMPTY = EMPTY
 ---@param adapter oil.Adapter
 ---@param col_def oil.ColumnSpec
 ---@param entry oil.InternalEntry
+---@param bufnr integer
 ---@return oil.TextChunk
-M.render_col = function(adapter, col_def, entry)
+M.render_col = function(adapter, col_def, entry, bufnr)
   local name, conf = util.split_config(col_def)
   local column = M.get_column(adapter, name)
   if not column then
@@ -69,7 +70,7 @@ M.render_col = function(adapter, col_def, entry)
     return EMPTY
   end
 
-  local chunk = column.render(entry, conf)
+  local chunk = column.render(entry, conf, bufnr)
   if type(chunk) == "table" then
     if chunk[1]:match("^%s*$") then
       return EMPTY
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index dd66842..0e605ac 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -747,7 +747,7 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden
   table.insert(cols, id_key)
   -- Then add all the configured columns
   for i, column in ipairs(column_defs) do
-    local chunk = columns.render_col(adapter, column, entry)
+    local chunk = columns.render_col(adapter, column, entry, bufnr)
     local text = type(chunk) == "table" and chunk[1] or chunk
     ---@cast text string
     col_width[i + 1] = math.max(col_width[i + 1], vim.api.nvim_strwidth(text))

From 5313690956d27cc6b53d5a2583df05e717c59b16 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 12 Feb 2025 22:12:17 -0800
Subject: [PATCH 158/206] fix: more robust parsing of custom column timestamp
 formats (#582)

---
 lua/oil/adapters/files.lua | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index b7f5363..40e82f5 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -180,7 +180,10 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
       local fmt = conf and conf.format
       local pattern
       if fmt then
-        pattern = fmt:gsub("%%.", "%%S+")
+        -- Replace placeholders with a pattern that matches non-space characters (e.g. %H -> %S+)
+        -- and whitespace with a pattern that matches any amount of whitespace
+        -- e.g. "%b %d %Y" -> "%S+%s+%S+%s+%S+"
+        pattern = fmt:gsub("%%.", "%%S+"):gsub("%s+", "%%s+")
       else
         pattern = "%S+%s+%d+%s+%d%d:?%d%d"
       end

From 32dd3e378d47673679e76a773451f82f971a66df Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 13 Feb 2025 09:40:01 -0800
Subject: [PATCH 159/206] feat: most moves and copies will copy the undofile
 (#583)

---
 lua/oil/adapters/trash/freedesktop.lua |  8 +++---
 lua/oil/adapters/trash/mac.lua         |  1 -
 lua/oil/fs.lua                         | 36 ++++++++++++++++++++++++++
 lua/oil/util.lua                       | 12 +++++++++
 4 files changed, 51 insertions(+), 6 deletions(-)

diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua
index 07e8a7f..c669730 100644
--- a/lua/oil/adapters/trash/freedesktop.lua
+++ b/lua/oil/adapters/trash/freedesktop.lua
@@ -151,7 +151,7 @@ end
 ---@field info_file string
 ---@field original_path string
 ---@field deletion_date number
----@field stat uv_fs_t
+---@field stat uv.aliases.fs_stat_table
 
 ---@param info_file string
 ---@param cb fun(err?: string, info?: oil.TrashInfo)
@@ -596,8 +596,7 @@ M.perform_action = function(action, cb)
         if err then
           cb(err)
         else
-          ---@diagnostic disable-next-line: undefined-field
-          local stat_type = trash_info.stat.type
+          local stat_type = trash_info.stat.type or "unknown"
           fs.recursive_copy(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
         end
       end)
@@ -625,8 +624,7 @@ M.delete_to_trash = function(path, cb)
     if err then
       cb(err)
     else
-      ---@diagnostic disable-next-line: undefined-field
-      local stat_type = trash_info.stat.type
+      local stat_type = trash_info.stat.type or "unknown"
       fs.recursive_move(stat_type, path, trash_info.trash_file, vim.schedule_wrap(cb))
     end
   end)
diff --git a/lua/oil/adapters/trash/mac.lua b/lua/oil/adapters/trash/mac.lua
index 8b2d33a..66cf4c1 100644
--- a/lua/oil/adapters/trash/mac.lua
+++ b/lua/oil/adapters/trash/mac.lua
@@ -224,7 +224,6 @@ M.delete_to_trash = function(path, cb)
       end
 
       local stat_type = src_stat.type
-      ---@cast stat_type oil.EntryType
       fs.recursive_move(stat_type, path, dest, vim.schedule_wrap(cb))
     end)
   )
diff --git a/lua/oil/fs.lua b/lua/oil/fs.lua
index ac216a1..a9ae10c 100644
--- a/lua/oil/fs.lua
+++ b/lua/oil/fs.lua
@@ -1,3 +1,4 @@
+local log = require("oil.log")
 local M = {}
 
 local uv = vim.uv or vim.loop
@@ -245,6 +246,37 @@ M.recursive_delete = function(entry_type, path, cb)
   end, 10000)
 end
 
+---Move the undofile for the file at src_path to dest_path
+---@param src_path string
+---@param dest_path string
+---@param copy boolean
+local move_undofile = vim.schedule_wrap(function(src_path, dest_path, copy)
+  local undofile = vim.fn.undofile(src_path)
+  uv.fs_stat(
+    undofile,
+    vim.schedule_wrap(function(stat_err)
+      if stat_err then
+        -- undofile doesn't exist
+        return
+      end
+      local dest_undofile = vim.fn.undofile(dest_path)
+      if copy then
+        uv.fs_copyfile(src_path, dest_path, function(err)
+          if err then
+            log.warn("Error copying undofile %s: %s", undofile, err)
+          end
+        end)
+      else
+        uv.fs_rename(undofile, dest_undofile, function(err)
+          if err then
+            log.warn("Error moving undofile %s: %s", undofile, err)
+          end
+        end)
+      end
+    end)
+  )
+end)
+
 ---@param entry_type oil.EntryType
 ---@param src_path string
 ---@param dest_path string
@@ -262,6 +294,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
   end
   if entry_type ~= "directory" then
     uv.fs_copyfile(src_path, dest_path, { excl = true }, cb)
+    move_undofile(src_path, dest_path, true)
     return
   end
   uv.fs_stat(src_path, function(stat_err, src_stat)
@@ -333,6 +366,9 @@ M.recursive_move = function(entry_type, src_path, dest_path, cb)
         end
       end)
     else
+      if entry_type ~= "directory" then
+        move_undofile(src_path, dest_path, false)
+      end
       cb()
     end
   end)
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 707aa24..7be1d5e 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -195,6 +195,18 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
       -- Try to delete, but don't if the buffer has changes
       pcall(vim.api.nvim_buf_delete, src_bufnr, {})
     end
+    -- Renaming a buffer won't load the undo file, so we need to do that manually
+    if vim.bo[dest_bufnr].undofile then
+      vim.api.nvim_buf_call(dest_bufnr, function()
+        vim.cmd.rundo({
+          args = { vim.fn.undofile(dest_buf_name) },
+          magic = { file = false, bar = false },
+          mods = {
+            emsg_silent = true,
+          },
+        })
+      end)
+    end
   end)
   return true
 end

From 7cde5aab10f564408e9ac349d457d755422d58cd Mon Sep 17 00:00:00 2001
From: forestchen1224 
Date: Fri, 14 Feb 2025 06:22:54 +0800
Subject: [PATCH 160/206] fix: disable_preview respected when preview_method !=
 "load" (#577)

* fix bug of disable_preview

file should not loaded if disable_preview is true

* refeactor function open_preview about disable_preview

switch the condition checking `disable_preview` of `if`
move the longer condition to the `elseif`
swap their repective code blocks to maintain the same functionality

* refactor: simplify conditionals

* fix: missing then

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 lua/oil/init.lua | 25 +++++++++++++------------
 1 file changed, 13 insertions(+), 12 deletions(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index e11400d..5f780b8 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -556,18 +556,19 @@ M.open_preview = function(opts, callback)
 
     local entry_is_file = not vim.endswith(normalized_url, "/")
     local filebufnr
-    if
-      entry_is_file
-      and config.preview_win.preview_method ~= "load"
-      and not util.file_matches_bufreadcmd(normalized_url)
-    then
-      filebufnr =
-        util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method)
-    elseif entry_is_file and config.preview_win.disable_preview(normalized_url) then
-      filebufnr = vim.api.nvim_create_buf(false, true)
-      vim.bo[filebufnr].bufhidden = "wipe"
-      vim.bo[filebufnr].buftype = "nofile"
-      util.render_text(filebufnr, "Preview disabled", { winid = preview_win })
+    if entry_is_file then
+      if config.preview_win.disable_preview(normalized_url) then
+        filebufnr = vim.api.nvim_create_buf(false, true)
+        vim.bo[filebufnr].bufhidden = "wipe"
+        vim.bo[filebufnr].buftype = "nofile"
+        util.render_text(filebufnr, "Preview disabled", { winid = preview_win })
+      elseif
+        config.preview_win.preview_method ~= "load"
+        and not util.file_matches_bufreadcmd(normalized_url)
+      then
+        filebufnr =
+          util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method)
+      end
     end
 
     if not filebufnr then

From 975a77cce3c8cb742bc1b3629f4328f5ca977dad Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 15 Feb 2025 14:15:32 -0800
Subject: [PATCH 161/206] chore(master): release 2.15.0 (#545)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
 CHANGELOG.md | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 90dfc95..458b3cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,34 @@
 # Changelog
 
+## [2.15.0](https://github.com/stevearc/oil.nvim/compare/v2.14.0...v2.15.0) (2025-02-13)
+
+
+### Features
+
+* add support for bufnr in column rendering functions ([#575](https://github.com/stevearc/oil.nvim/issues/575)) ([8abc58b](https://github.com/stevearc/oil.nvim/commit/8abc58b038f84078121ab1cac6ecad0163fe1635))
+* API to automatically open preview window after opening oil ([#339](https://github.com/stevearc/oil.nvim/issues/339)) ([57528bf](https://github.com/stevearc/oil.nvim/commit/57528bf9c58080ca891e8d362d0a578895c136ce))
+* can selectively add entries to quickfix ([#564](https://github.com/stevearc/oil.nvim/issues/564)) ([b594b9a](https://github.com/stevearc/oil.nvim/commit/b594b9a9052618669ccf6520b2d0c0d942eb8118))
+* floating window max width/height can be percentages ([#553](https://github.com/stevearc/oil.nvim/issues/553)) ([1df90fa](https://github.com/stevearc/oil.nvim/commit/1df90faf927e78f5aacf278abd0bfdcb5f45e825))
+* most moves and copies will copy the undofile ([#583](https://github.com/stevearc/oil.nvim/issues/583)) ([32dd3e3](https://github.com/stevearc/oil.nvim/commit/32dd3e378d47673679e76a773451f82f971a66df))
+* pass oil bufnr to custom filename highlight function ([#552](https://github.com/stevearc/oil.nvim/issues/552)) ([f5c563a](https://github.com/stevearc/oil.nvim/commit/f5c563a074a38cee5a09f98e98b74dcd2c322490))
+
+
+### Bug Fixes
+
+* crash in preview on nvim 0.8 ([81b2c5f](https://github.com/stevearc/oil.nvim/commit/81b2c5f04ae24a8c83b20ecbd017fecac15faca0))
+* directory rendering with custom highlights ([#551](https://github.com/stevearc/oil.nvim/issues/551)) ([a6a4f48](https://github.com/stevearc/oil.nvim/commit/a6a4f48b14b4a51fded531c86f6c04b4503a2ef8))
+* disable_preview respected when preview_method != "load" ([#577](https://github.com/stevearc/oil.nvim/issues/577)) ([7cde5aa](https://github.com/stevearc/oil.nvim/commit/7cde5aab10f564408e9ac349d457d755422d58cd))
+* error when non-current oil buffer has validation errors ([#561](https://github.com/stevearc/oil.nvim/issues/561)) ([8d11a2a](https://github.com/stevearc/oil.nvim/commit/8d11a2abf3039b1974d4acd65fbc83ada2ca1084))
+* gracefully handle fs_stat failures ([#558](https://github.com/stevearc/oil.nvim/issues/558)) ([7c26a59](https://github.com/stevearc/oil.nvim/commit/7c26a59ac0061b199bf9f44b19d45cfadd9b14f5))
+* guard against nil metadata values ([#548](https://github.com/stevearc/oil.nvim/issues/548)) ([254bc66](https://github.com/stevearc/oil.nvim/commit/254bc6635cb3f77e6e9a89155652f368e5535160))
+* more consistent cursor position when entering a new directory ([#536](https://github.com/stevearc/oil.nvim/issues/536)) ([c80fa5c](https://github.com/stevearc/oil.nvim/commit/c80fa5c415b882c1c694a32748cea09b7dafc2c5))
+* more robust parsing of custom column timestamp formats ([#582](https://github.com/stevearc/oil.nvim/issues/582)) ([5313690](https://github.com/stevearc/oil.nvim/commit/5313690956d27cc6b53d5a2583df05e717c59b16))
+* open files in correct window from floating oil ([#560](https://github.com/stevearc/oil.nvim/issues/560)) ([83ac518](https://github.com/stevearc/oil.nvim/commit/83ac5185f79ab8d869bccea792dc516ad02ad06e))
+* preview sometimes causes oil buffers to be stuck in unloaded state ([#563](https://github.com/stevearc/oil.nvim/issues/563)) ([1488f0d](https://github.com/stevearc/oil.nvim/commit/1488f0d96b1cb820dd12f05a7bf5283a631a7c4d))
+* stat files if fs_readdir doesn't provide a type ([#543](https://github.com/stevearc/oil.nvim/issues/543)) ([c6a39a6](https://github.com/stevearc/oil.nvim/commit/c6a39a69b2df7c10466f150dde0bd23e49c1fba3))
+* support permissions checks on windows and virtual filesystems ([#555](https://github.com/stevearc/oil.nvim/issues/555)) ([7041528](https://github.com/stevearc/oil.nvim/commit/7041528bdedb350ad66e650684deec8456e053cc))
+* work around incorrect link detection on windows ([#557](https://github.com/stevearc/oil.nvim/issues/557)) ([09fa1d2](https://github.com/stevearc/oil.nvim/commit/09fa1d22f5edf0730824d2b222d726c8c81bbdc9))
+
 ## [2.14.0](https://github.com/stevearc/oil.nvim/compare/v2.13.0...v2.14.0) (2024-12-21)
 
 

From d7c61c70849ec99f005615c4175118986f200e4f Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Tue, 4 Mar 2025 12:56:54 -0800
Subject: [PATCH 162/206] fix: silent handling when buffer has no oil adapter
 (#573)

---
 lua/oil/adapters/files.lua |  6 +++---
 lua/oil/adapters/ssh.lua   |  8 ++++----
 lua/oil/config.lua         |  5 -----
 lua/oil/init.lua           |  5 ++---
 lua/oil/mutator/init.lua   |  4 ++--
 lua/oil/mutator/parser.lua |  2 +-
 lua/oil/util.lua           | 12 +++++-------
 lua/oil/view.lua           | 12 ++++++------
 8 files changed, 23 insertions(+), 31 deletions(-)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 40e82f5..8fb1835 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -530,7 +530,7 @@ M.render_action = function(action)
       return string.format("DELETE %s", short_path)
     end
   elseif action.type == "move" or action.type == "copy" then
-    local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
+    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
     if dest_adapter == M then
       local _, src_path = util.parse_url(action.src_url)
       assert(src_path)
@@ -623,7 +623,7 @@ M.perform_action = function(action, cb)
       fs.recursive_delete(action.entry_type, path, cb)
     end
   elseif action.type == "move" then
-    local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
+    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
     if dest_adapter == M then
       local _, src_path = util.parse_url(action.src_url)
       assert(src_path)
@@ -641,7 +641,7 @@ M.perform_action = function(action, cb)
       cb("files adapter doesn't support cross-adapter move")
     end
   elseif action.type == "copy" then
-    local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
+    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
     if dest_adapter == M then
       local _, src_path = util.parse_url(action.src_url)
       assert(src_path)
diff --git a/lua/oil/adapters/ssh.lua b/lua/oil/adapters/ssh.lua
index 02637a8..ae4291f 100644
--- a/lua/oil/adapters/ssh.lua
+++ b/lua/oil/adapters/ssh.lua
@@ -303,8 +303,8 @@ M.perform_action = function(action, cb)
     local conn = get_connection(action.url)
     conn:rm(res.path, cb)
   elseif action.type == "move" then
-    local src_adapter = config.get_adapter_by_scheme(action.src_url)
-    local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
+    local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
+    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
     if src_adapter == M and dest_adapter == M then
       local src_res = M.parse_url(action.src_url)
       local dest_res = M.parse_url(action.dest_url)
@@ -324,8 +324,8 @@ M.perform_action = function(action, cb)
       cb("We should never attempt to move across adapters")
     end
   elseif action.type == "copy" then
-    local src_adapter = config.get_adapter_by_scheme(action.src_url)
-    local dest_adapter = config.get_adapter_by_scheme(action.dest_url)
+    local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
+    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
     if src_adapter == M and dest_adapter == M then
       local src_res = M.parse_url(action.src_url)
       local dest_res = M.parse_url(action.dest_url)
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 562562c..6797cc8 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -464,10 +464,6 @@ M.get_adapter_by_scheme = function(scheme)
   if adapter == nil then
     local name = M.adapters[scheme]
     if not name then
-      vim.notify(
-        string.format("Could not find oil adapter for scheme '%s'", scheme),
-        vim.log.levels.ERROR
-      )
       return nil
     end
     local ok
@@ -478,7 +474,6 @@ M.get_adapter_by_scheme = function(scheme)
     else
       M._adapter_by_scheme[scheme] = false
       adapter = false
-      vim.notify(string.format("Could not find oil adapter '%s'", name), vim.log.levels.ERROR)
     end
   end
   if adapter then
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 5f780b8..ca55dc3 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -224,7 +224,7 @@ M.get_buffer_parent_url = function(bufname, use_oil_parent)
     if not use_oil_parent then
       return bufname
     end
-    local adapter = config.get_adapter_by_scheme(scheme)
+    local adapter = assert(config.get_adapter_by_scheme(scheme))
     local parent_url
     if adapter and adapter.get_parent then
       local adapter_scheme = config.adapter_to_scheme[adapter.name]
@@ -1253,8 +1253,7 @@ M.setup = function(opts)
         end)
         vim.cmd.doautocmd({ args = { "BufWritePost", params.file }, mods = { silent = true } })
       else
-        local adapter = config.get_adapter_by_scheme(bufname)
-        assert(adapter)
+        local adapter = assert(config.get_adapter_by_scheme(bufname))
         adapter.write_file(params.buf)
       end
     end,
diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua
index cd0043f..f55957d 100644
--- a/lua/oil/mutator/init.lua
+++ b/lua/oil/mutator/init.lua
@@ -85,7 +85,7 @@ M.create_actions_from_diffs = function(all_diffs)
     end
   end
   for bufnr, diffs in pairs(all_diffs) do
-    local adapter = util.get_adapter(bufnr)
+    local adapter = util.get_adapter(bufnr, true)
     if not adapter then
       error("Missing adapter")
     end
@@ -519,7 +519,7 @@ M.try_write_changes = function(confirm, cb)
     if vim.bo[bufnr].modified then
       local diffs, errors = parser.parse(bufnr)
       all_diffs[bufnr] = diffs
-      local adapter = assert(util.get_adapter(bufnr))
+      local adapter = assert(util.get_adapter(bufnr, true))
       if adapter.filter_error then
         errors = vim.tbl_filter(adapter.filter_error, errors)
       end
diff --git a/lua/oil/mutator/parser.lua b/lua/oil/mutator/parser.lua
index d2d3590..f25cbc9 100644
--- a/lua/oil/mutator/parser.lua
+++ b/lua/oil/mutator/parser.lua
@@ -156,7 +156,7 @@ M.parse = function(bufnr)
   ---@type oil.ParseError[]
   local errors = {}
   local bufname = vim.api.nvim_buf_get_name(bufnr)
-  local adapter = util.get_adapter(bufnr)
+  local adapter = util.get_adapter(bufnr, true)
   if not adapter then
     table.insert(errors, {
       lnum = 0,
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 7be1d5e..ed8bce7 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -60,11 +60,12 @@ M.url_escape = function(string)
 end
 
 ---@param bufnr integer
+---@param silent? boolean
 ---@return nil|oil.Adapter
-M.get_adapter = function(bufnr)
+M.get_adapter = function(bufnr, silent)
   local bufname = vim.api.nvim_buf_get_name(bufnr)
   local adapter = config.get_adapter_by_scheme(bufname)
-  if not adapter then
+  if not adapter and not silent then
     vim.notify_once(
       string.format("[oil] could not find adapter for buffer '%s://'", bufname),
       vim.log.levels.ERROR
@@ -500,10 +501,7 @@ end
 ---@return oil.Adapter
 ---@return nil|oil.CrossAdapterAction
 M.get_adapter_for_action = function(action)
-  local adapter = config.get_adapter_by_scheme(action.url or action.src_url)
-  if not adapter then
-    error("no adapter found")
-  end
+  local adapter = assert(config.get_adapter_by_scheme(action.url or action.src_url))
   if action.dest_url then
     local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
     if adapter ~= dest_adapter then
@@ -887,7 +885,7 @@ M.get_edit_path = function(bufnr, entry, callback)
 
   local bufname = vim.api.nvim_buf_get_name(bufnr)
   local scheme, dir = M.parse_url(bufname)
-  local adapter = M.get_adapter(bufnr)
+  local adapter = M.get_adapter(bufnr, true)
   assert(scheme and dir and adapter)
 
   local url = scheme .. dir .. entry.name
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 0e605ac..271705d 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -146,7 +146,7 @@ M.unlock_buffers = function()
   buffers_locked = false
   for bufnr in pairs(session) do
     if vim.api.nvim_buf_is_loaded(bufnr) then
-      local adapter = util.get_adapter(bufnr)
+      local adapter = util.get_adapter(bufnr, true)
       if adapter then
         vim.bo[bufnr].modifiable = adapter.is_modifiable(bufnr)
       end
@@ -265,7 +265,7 @@ local function constrain_cursor(mode)
   end
   local parser = require("oil.mutator.parser")
 
-  local adapter = util.get_adapter(0)
+  local adapter = util.get_adapter(0, true)
   if not adapter then
     return
   end
@@ -296,7 +296,7 @@ local function redraw_trash_virtual_text(bufnr)
     return
   end
   local parser = require("oil.mutator.parser")
-  local adapter = util.get_adapter(bufnr)
+  local adapter = util.get_adapter(bufnr, true)
   if not adapter or adapter.name ~= "trash" then
     return
   end
@@ -456,7 +456,7 @@ M.initialize = function(bufnr)
     end,
   })
 
-  local adapter = util.get_adapter(bufnr)
+  local adapter = util.get_adapter(bufnr, true)
 
   -- Set up a watcher that will refresh the directory
   if
@@ -616,7 +616,7 @@ local function render_buffer(bufnr, opts)
     jump_first = false,
   })
   local scheme = util.parse_url(bufname)
-  local adapter = util.get_adapter(bufnr)
+  local adapter = util.get_adapter(bufnr, true)
   if not scheme or not adapter then
     return false
   end
@@ -877,7 +877,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
     handle_error(string.format("Could not parse oil url '%s'", bufname))
     return
   end
-  local adapter = util.get_adapter(bufnr)
+  local adapter = util.get_adapter(bufnr, true)
   if not adapter then
     handle_error(string.format("[oil] no adapter for buffer '%s'", bufname))
     return

From 54fe7dca365e2b917ee269744055320c1f29380d Mon Sep 17 00:00:00 2001
From: skshetry <18718008+skshetry@users.noreply.github.com>
Date: Wed, 5 Mar 2025 06:29:26 +0545
Subject: [PATCH 163/206] fix: pass bufnr to constrain_cursor (#574)

* pass bufnr to the constrain_cursor

* return early if the oil buffer is not the current buffer

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 lua/oil/view.lua | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 271705d..c2cb076 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -258,20 +258,24 @@ local function get_first_mutable_column_col(adapter, ranges)
 end
 
 ---Force cursor to be after hidden/immutable columns
+---@param bufnr integer
 ---@param mode false|"name"|"editable"
-local function constrain_cursor(mode)
+local function constrain_cursor(bufnr, mode)
   if not mode then
     return
   end
+  if bufnr ~= vim.api.nvim_get_current_buf() then
+    return
+  end
   local parser = require("oil.mutator.parser")
 
-  local adapter = util.get_adapter(0, true)
+  local adapter = util.get_adapter(bufnr, true)
   if not adapter then
     return
   end
 
   local cur = vim.api.nvim_win_get_cursor(0)
-  local line = vim.api.nvim_buf_get_lines(0, cur[1] - 1, cur[1], true)[1]
+  local line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1]
   local column_defs = columns.get_supported_columns(adapter)
   local result = parser.parse_line(adapter, line, column_defs)
   if result and result.ranges then
@@ -406,7 +410,7 @@ M.initialize = function(bufnr)
     callback = function()
       -- For some reason the cursor bounces back to its original position,
       -- so we have to defer the call
-      vim.schedule_wrap(constrain_cursor)(config.constrain_cursor)
+      vim.schedule_wrap(constrain_cursor)(bufnr, config.constrain_cursor)
     end,
   })
   vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
@@ -419,7 +423,7 @@ M.initialize = function(bufnr)
         return
       end
 
-      constrain_cursor(config.constrain_cursor)
+      constrain_cursor(bufnr, config.constrain_cursor)
 
       if config.preview_win.update_on_cursor_moved then
         -- Debounce and update the preview window
@@ -690,7 +694,7 @@ local function render_buffer(bufnr, opts)
             end
           end
 
-          constrain_cursor("name")
+          constrain_cursor(bufnr, "name")
         end
       end
     end)

From 548587d68b55e632d8a69c92cefd981f360634fa Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Tue, 4 Mar 2025 22:12:47 -0800
Subject: [PATCH 164/206] fix: better detection of oil buffers (#589)

---
 lua/oil/util.lua | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index ed8bce7..8f86b64 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -654,8 +654,12 @@ end
 ---@param bufnr integer
 ---@return boolean
 M.is_oil_bufnr = function(bufnr)
-  if vim.bo[bufnr].filetype == "oil" then
+  local filetype = vim.bo[bufnr].filetype
+  if filetype == "oil" then
     return true
+  elseif filetype ~= "" then
+    -- If the filetype is set and is NOT "oil", then it's not an oil buffer
+    return false
   end
   local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr))
   return config.adapters[scheme] or config.adapter_aliases[scheme]

From 8649818fb29322a8ee24c5cd2cd7b2f6c40258a3 Mon Sep 17 00:00:00 2001
From: Luis Calle <53507599+TheLeoP@users.noreply.github.com>
Date: Wed, 19 Mar 2025 17:10:58 -0500
Subject: [PATCH 165/206] fix(trash-win): don't hang when `shellslash` is
 enabled (#592)

---
 .../adapters/trash/windows/powershell-connection.lua  | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/lua/oil/adapters/trash/windows/powershell-connection.lua b/lua/oil/adapters/trash/windows/powershell-connection.lua
index a296a7e..332defb 100644
--- a/lua/oil/adapters/trash/windows/powershell-connection.lua
+++ b/lua/oil/adapters/trash/windows/powershell-connection.lua
@@ -28,6 +28,16 @@ end
 
 ---@param init_command? string
 function PowershellConnection:_init(init_command)
+  -- For some reason beyond my understanding, at least one of the following
+  -- things requires `noshellslash` to avoid the embeded powershell process to
+  -- send only "" to the stdout (never calling the callback because
+  -- "===DONE(True)===" is never sent to stdout)
+  -- * vim.fn.jobstart
+  -- * cmd.exe
+  -- * powershell.exe
+  local saved_shellslash = vim.o.shellslash
+  vim.o.shellslash = false
+
   -- 65001 is the UTF-8 codepage
   -- powershell needs to be launched with the UTF-8 codepage to use it for both stdin and stdout
   local jid = vim.fn.jobstart({
@@ -57,6 +67,7 @@ function PowershellConnection:_init(init_command)
       end
     end,
   })
+  vim.o.shellslash = saved_shellslash
 
   if jid == 0 then
     self:_set_error("passed invalid arguments to 'powershell'")

From 4c9bdf0d839932617cdb25ed46a2f7bb1e090f77 Mon Sep 17 00:00:00 2001
From: Steve Walker <65963536+stevalkr@users.noreply.github.com>
Date: Thu, 20 Mar 2025 23:19:18 +0800
Subject: [PATCH 166/206] feat: copy/paste to system clipboard (#559)

* feat: copy/paste to system clipboard on macOS

* stylua

* feat: copy/paste to system clipboard on linux

* force mime type

* fix string.gsub

* vim.uv or vim.loop

* fix stylua

* support gnome directly

* support wayland

* refactor: extract clipboard actions into separate file

* fix: copy/paste in KDE

* refactor: simplify file loading

* fix: copy/paste on x11

* fix: better error message when clipboard command not found

* fix: paste on mac

* fix: pasting in Gnome

* feat: support pasting multiple files

* feat: support copying multiple files to clipboard

---------

Co-authored-by: Steve Walker <65963536+etherswangel@users.noreply.github.com>
Co-authored-by: Steven Arcangeli 
---
 doc/oil.txt           |   6 +
 lua/oil/actions.lua   |  14 ++
 lua/oil/clipboard.lua | 328 ++++++++++++++++++++++++++++++++++++++++++
 lua/oil/init.lua      |  13 +-
 lua/oil/util.lua      |  72 ++++++----
 tests/util_spec.lua   |  29 ++++
 6 files changed, 427 insertions(+), 35 deletions(-)
 create mode 100644 lua/oil/clipboard.lua
 create mode 100644 tests/util_spec.lua

diff --git a/doc/oil.txt b/doc/oil.txt
index fea9bfd..fdc7ca5 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -545,6 +545,9 @@ close                                                              *actions.clos
     Parameters:
       {exit_if_last_buf} `boolean` Exit vim if oil is closed as the last buffer
 
+copy_to_system_clipboard                        *actions.copy_to_system_clipboard*
+    Copy the entry under the cursor to the system clipboard
+
 open_cmdline                                                *actions.open_cmdline*
     Open vim cmdline with current entry as an argument
 
@@ -565,6 +568,9 @@ open_terminal                                              *actions.open_termina
 parent                                                            *actions.parent*
     Navigate to the parent path
 
+paste_from_system_clipboard                  *actions.paste_from_system_clipboard*
+    Paste the system clipboard into the current oil directory
+
 preview                                                          *actions.preview*
     Open the entry under the cursor in a preview window, or close the preview
     window if already open
diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua
index cef37c7..6315b68 100644
--- a/lua/oil/actions.lua
+++ b/lua/oil/actions.lua
@@ -418,6 +418,20 @@ M.copy_entry_filename = {
   end,
 }
 
+M.copy_to_system_clipboard = {
+  desc = "Copy the entry under the cursor to the system clipboard",
+  callback = function()
+    require("oil.clipboard").copy_to_system_clipboard()
+  end,
+}
+
+M.paste_from_system_clipboard = {
+  desc = "Paste the system clipboard into the current oil directory",
+  callback = function()
+    require("oil.clipboard").paste_from_system_clipboard()
+  end,
+}
+
 M.open_cmdline_dir = {
   desc = "Open vim cmdline with current directory as an argument",
   deprecated = true,
diff --git a/lua/oil/clipboard.lua b/lua/oil/clipboard.lua
new file mode 100644
index 0000000..32bcfdb
--- /dev/null
+++ b/lua/oil/clipboard.lua
@@ -0,0 +1,328 @@
+local cache = require("oil.cache")
+local columns = require("oil.columns")
+local config = require("oil.config")
+local fs = require("oil.fs")
+local oil = require("oil")
+local util = require("oil.util")
+local view = require("oil.view")
+
+local M = {}
+
+---@return "wayland"|"x11"|nil
+local function get_linux_session_type()
+  local xdg_session_type = vim.env.XDG_SESSION_TYPE:lower()
+  if xdg_session_type:find("x11") then
+    return "x11"
+  elseif xdg_session_type:find("wayland") then
+    return "wayland"
+  else
+    return nil
+  end
+end
+
+---@return boolean
+local function is_linux_desktop_gnome()
+  local cur_desktop = vim.env.XDG_CURRENT_DESKTOP
+  local idx = vim.env.XDG_SESSION_DESKTOP:lower():find("gnome") or cur_desktop:lower():find("gnome")
+  return idx ~= nil or cur_desktop == "X-Cinnamon" or cur_desktop == "XFCE"
+end
+
+---@param winid integer
+---@param entry oil.InternalEntry
+---@param column_defs oil.ColumnSpec[]
+---@param adapter oil.Adapter
+---@param bufnr integer
+local function write_pasted(winid, entry, column_defs, adapter, bufnr)
+  local col_width = {}
+  for i in ipairs(column_defs) do
+    col_width[i + 1] = 1
+  end
+  local line_table =
+    { view.format_entry_cols(entry, column_defs, col_width, adapter, false, bufnr) }
+  local lines, _ = util.render_table(line_table, col_width)
+  local pos = vim.api.nvim_win_get_cursor(winid)
+  vim.api.nvim_buf_set_lines(bufnr, pos[1], pos[1], true, lines)
+end
+
+---@param paths string[]
+local function paste_paths(paths)
+  local bufnr = vim.api.nvim_get_current_buf()
+  local scheme = "oil://"
+  local adapter = assert(config.get_adapter_by_scheme(scheme))
+  local column_defs = columns.get_supported_columns(scheme)
+  local winid = vim.api.nvim_get_current_win()
+
+  local parent_urls = {}
+  local pending_paths = {}
+
+  for _, path in ipairs(paths) do
+    -- Trim the trailing slash off directories
+    if vim.endswith(path, "/") then
+      path = path:sub(1, -2)
+    end
+
+    local ori_entry = cache.get_entry_by_url(scheme .. path)
+    if ori_entry then
+      write_pasted(winid, ori_entry, column_defs, adapter, bufnr)
+    else
+      local parent_url = scheme .. vim.fs.dirname(path)
+      parent_urls[parent_url] = true
+      table.insert(pending_paths, path)
+    end
+  end
+  if #pending_paths == 0 then
+    return
+  end
+
+  local cursor = vim.api.nvim_win_get_cursor(winid)
+  local complete_loading = util.cb_collect(#vim.tbl_keys(parent_urls), function(err)
+    if err then
+      vim.notify(string.format("Error loading parent directory: %s", err), vim.log.levels.ERROR)
+    else
+      -- Something in this process moves the cursor to the top of the window, so have to restore it
+      vim.api.nvim_win_set_cursor(winid, cursor)
+
+      for _, path in ipairs(pending_paths) do
+        local ori_entry = cache.get_entry_by_url(scheme .. path)
+        if ori_entry then
+          write_pasted(winid, ori_entry, column_defs, adapter, bufnr)
+        else
+          vim.notify(
+            string.format("The pasted file '%s' could not be found", path),
+            vim.log.levels.ERROR
+          )
+        end
+      end
+    end
+  end)
+
+  for parent_url, _ in pairs(parent_urls) do
+    local new_bufnr = vim.api.nvim_create_buf(false, false)
+    vim.api.nvim_buf_set_name(new_bufnr, parent_url)
+    oil.load_oil_buffer(new_bufnr)
+    util.run_after_load(new_bufnr, complete_loading)
+  end
+end
+
+---@return integer start
+---@return integer end
+local function range_from_selection()
+  -- [bufnum, lnum, col, off]; both row and column 1-indexed
+  local start = vim.fn.getpos("v")
+  local end_ = vim.fn.getpos(".")
+  local start_row = start[2]
+  local end_row = end_[2]
+
+  if start_row > end_row then
+    start_row, end_row = end_row, start_row
+  end
+
+  return start_row, end_row
+end
+
+M.copy_to_system_clipboard = function()
+  local dir = oil.get_current_dir()
+  if not dir then
+    vim.notify("System clipboard only works for local files", vim.log.levels.ERROR)
+    return
+  end
+
+  local entries = {}
+  local mode = vim.api.nvim_get_mode().mode
+  if mode == "v" or mode == "V" then
+    if fs.is_mac then
+      vim.notify(
+        "Copying multiple paths to clipboard is not supported on mac",
+        vim.log.levels.ERROR
+      )
+      return
+    end
+    local start_row, end_row = range_from_selection()
+    for i = start_row, end_row do
+      table.insert(entries, oil.get_entry_on_line(0, i))
+    end
+
+    -- leave visual mode
+    vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", true)
+  else
+    table.insert(entries, oil.get_cursor_entry())
+  end
+
+  -- This removes holes in the list-like table
+  entries = vim.tbl_values(entries)
+
+  if #entries == 0 then
+    vim.notify("Could not find local file under cursor", vim.log.levels.WARN)
+    return
+  end
+  local paths = {}
+  for _, entry in ipairs(entries) do
+    table.insert(paths, dir .. entry.name)
+  end
+  local cmd = {}
+  local stdin
+  if fs.is_mac then
+    cmd = {
+      "osascript",
+      "-e",
+      "on run args",
+      "-e",
+      "set the clipboard to POSIX file (first item of args)",
+      "-e",
+      "end run",
+      paths[1],
+    }
+  elseif fs.is_linux then
+    local xdg_session_type = get_linux_session_type()
+    if xdg_session_type == "x11" then
+      vim.list_extend(cmd, { "xclip", "-i", "-selection", "clipboard" })
+    elseif xdg_session_type == "wayland" then
+      table.insert(cmd, "wl-copy")
+    else
+      vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR)
+      return
+    end
+    local urls = {}
+    for _, path in ipairs(paths) do
+      table.insert(urls, "file://" .. path)
+    end
+    if is_linux_desktop_gnome() then
+      stdin = string.format("copy\n%s\0", table.concat(urls, "\n"))
+      vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" })
+    else
+      stdin = table.concat(urls, "\n") .. "\n"
+      vim.list_extend(cmd, { "-t", "text/uri-list" })
+    end
+  else
+    vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR)
+    return
+  end
+
+  if vim.fn.executable(cmd[1]) == 0 then
+    vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR)
+    return
+  end
+  local stderr = ""
+  local jid = vim.fn.jobstart(cmd, {
+    stderr_buffered = true,
+    on_stderr = function(_, data)
+      stderr = table.concat(data, "\n")
+    end,
+    on_exit = function(j, exit_code)
+      if exit_code ~= 0 then
+        vim.notify(
+          string.format("Error copying '%s' to system clipboard\n%s", vim.inspect(paths), stderr),
+          vim.log.levels.ERROR
+        )
+      else
+        if #paths == 1 then
+          vim.notify(string.format("Copied '%s' to system clipboard", paths[1]))
+        else
+          vim.notify(string.format("Copied %d files to system clipboard", #paths))
+        end
+      end
+    end,
+  })
+  assert(jid > 0, "Failed to start job")
+  if stdin then
+    vim.api.nvim_chan_send(jid, stdin)
+    vim.fn.chanclose(jid, "stdin")
+  end
+end
+
+---@param lines string[]
+---@return string[]
+local function handle_paste_output_mac(lines)
+  local ret = {}
+  for _, line in ipairs(lines) do
+    if not line:match("^%s*$") then
+      table.insert(ret, line)
+    end
+  end
+  return ret
+end
+
+---@param lines string[]
+---@return string[]
+local function handle_paste_output_linux(lines)
+  local ret = {}
+  for _, line in ipairs(lines) do
+    local path = line:match("^file://(.+)$")
+    if path then
+      table.insert(ret, util.url_unescape(path))
+    end
+  end
+  return ret
+end
+
+M.paste_from_system_clipboard = function()
+  local dir = oil.get_current_dir()
+  if not dir then
+    return
+  end
+  local cmd = {}
+  local handle_paste_output
+  if fs.is_mac then
+    cmd = {
+      "osascript",
+      "-e",
+      "on run",
+      "-e",
+      "POSIX path of (the clipboard as «class furl»)",
+      "-e",
+      "end run",
+    }
+    handle_paste_output = handle_paste_output_mac
+  elseif fs.is_linux then
+    local xdg_session_type = get_linux_session_type()
+    if xdg_session_type == "x11" then
+      vim.list_extend(cmd, { "xclip", "-o", "-selection", "clipboard" })
+    elseif xdg_session_type == "wayland" then
+      table.insert(cmd, "wl-paste")
+    else
+      vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR)
+      return
+    end
+    if is_linux_desktop_gnome() then
+      vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" })
+    else
+      vim.list_extend(cmd, { "-t", "text/uri-list" })
+    end
+    handle_paste_output = handle_paste_output_linux
+  else
+    vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR)
+    return
+  end
+  local paths
+  local stderr = ""
+  if vim.fn.executable(cmd[1]) == 0 then
+    vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR)
+    return
+  end
+  local jid = vim.fn.jobstart(cmd, {
+    stdout_buffered = true,
+    stderr_buffered = true,
+    on_stdout = function(j, data)
+      local lines = vim.split(table.concat(data, "\n"), "\r?\n")
+      paths = handle_paste_output(lines)
+    end,
+    on_stderr = function(_, data)
+      stderr = table.concat(data, "\n")
+    end,
+    on_exit = function(j, exit_code)
+      if exit_code ~= 0 or not paths then
+        vim.notify(
+          string.format("Error pasting from system clipboard: %s", stderr),
+          vim.log.levels.ERROR
+        )
+      elseif #paths == 0 then
+        vim.notify("No valid files found in system clipboard", vim.log.levels.WARN)
+      else
+        paste_paths(paths)
+      end
+    end,
+  })
+  assert(jid > 0, "Failed to start job")
+end
+
+return M
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index ca55dc3..2c10b10 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -30,8 +30,6 @@ local M = {}
 ---@field filter_action? fun(action: oil.Action): boolean When present, filter out actions as they are created
 ---@field filter_error? fun(action: oil.ParseError): boolean When present, filter out errors from parsing a buffer
 
-local load_oil_buffer
-
 ---Get the entry on a specific line (1-indexed)
 ---@param bufnr integer
 ---@param lnum integer
@@ -593,7 +591,7 @@ M.open_preview = function(opts, callback)
     -- If we called open_preview during an autocmd, then the edit command may not trigger the
     -- BufReadCmd to load the buffer. So we need to do it manually.
     if util.is_oil_bufnr(filebufnr) then
-      load_oil_buffer(filebufnr)
+      M.load_oil_buffer(filebufnr)
     end
 
     vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 })
@@ -1013,8 +1011,9 @@ local function restore_alt_buf()
   end
 end
 
+---@private
 ---@param bufnr integer
-load_oil_buffer = function(bufnr)
+M.load_oil_buffer = function(bufnr)
   local config = require("oil.config")
   local keymap_util = require("oil.keymap_util")
   local loading = require("oil.loading")
@@ -1218,7 +1217,7 @@ M.setup = function(opts)
     pattern = scheme_pattern,
     nested = true,
     callback = function(params)
-      load_oil_buffer(params.buf)
+      M.load_oil_buffer(params.buf)
     end,
   })
   vim.api.nvim_create_autocmd("BufWriteCmd", {
@@ -1388,7 +1387,7 @@ M.setup = function(opts)
       local util = require("oil.util")
       local scheme = util.parse_url(params.file)
       if config.adapters[scheme] and vim.api.nvim_buf_line_count(params.buf) == 1 then
-        load_oil_buffer(params.buf)
+        M.load_oil_buffer(params.buf)
       end
     end,
   })
@@ -1397,7 +1396,7 @@ M.setup = function(opts)
   if maybe_hijack_directory_buffer(bufnr) and vim.v.vim_did_enter == 1 then
     -- manually call load on a hijacked directory buffer if vim has already entered
     -- (the BufReadCmd will not trigger)
-    load_oil_buffer(bufnr)
+    M.load_oil_buffer(bufnr)
   end
 end
 
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 8f86b64..0ffebfc 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -25,38 +25,54 @@ M.escape_filename = function(filename)
   return ret
 end
 
-local _url_escape_chars = {
-  [" "] = "%20",
-  ["$"] = "%24",
-  ["&"] = "%26",
-  ["`"] = "%60",
-  [":"] = "%3A",
-  ["<"] = "%3C",
-  ["="] = "%3D",
-  [">"] = "%3E",
-  ["?"] = "%3F",
-  ["["] = "%5B",
-  ["\\"] = "%5C",
-  ["]"] = "%5D",
-  ["^"] = "%5E",
-  ["{"] = "%7B",
-  ["|"] = "%7C",
-  ["}"] = "%7D",
-  ["~"] = "%7E",
-  ["“"] = "%22",
-  ["‘"] = "%27",
-  ["+"] = "%2B",
-  [","] = "%2C",
-  ["#"] = "%23",
-  ["%"] = "%25",
-  ["@"] = "%40",
-  ["/"] = "%2F",
-  [";"] = "%3B",
+local _url_escape_to_char = {
+  ["20"] = " ",
+  ["22"] = "“",
+  ["23"] = "#",
+  ["24"] = "$",
+  ["25"] = "%",
+  ["26"] = "&",
+  ["27"] = "‘",
+  ["2B"] = "+",
+  ["2C"] = ",",
+  ["2F"] = "/",
+  ["3A"] = ":",
+  ["3B"] = ";",
+  ["3C"] = "<",
+  ["3D"] = "=",
+  ["3E"] = ">",
+  ["3F"] = "?",
+  ["40"] = "@",
+  ["5B"] = "[",
+  ["5C"] = "\\",
+  ["5D"] = "]",
+  ["5E"] = "^",
+  ["60"] = "`",
+  ["7B"] = "{",
+  ["7C"] = "|",
+  ["7D"] = "}",
+  ["7E"] = "~",
 }
+local _char_to_url_escape = {}
+for k, v in pairs(_url_escape_to_char) do
+  _char_to_url_escape[v] = "%" .. k
+end
+-- TODO this uri escape handling is very incomplete
+
 ---@param string string
 ---@return string
 M.url_escape = function(string)
-  return (string:gsub(".", _url_escape_chars))
+  return (string:gsub(".", _char_to_url_escape))
+end
+
+---@param string string
+---@return string
+M.url_unescape = function(string)
+  return (
+    string:gsub("%%([0-9A-Fa-f][0-9A-Fa-f])", function(seq)
+      return _url_escape_to_char[seq:upper()] or ("%" .. seq)
+    end)
+  )
 end
 
 ---@param bufnr integer
diff --git a/tests/util_spec.lua b/tests/util_spec.lua
new file mode 100644
index 0000000..3193842
--- /dev/null
+++ b/tests/util_spec.lua
@@ -0,0 +1,29 @@
+local util = require("oil.util")
+describe("util", function()
+  it("url_escape", function()
+    local cases = {
+      { "foobar", "foobar" },
+      { "foo bar", "foo%20bar" },
+      { "/foo/bar", "%2Ffoo%2Fbar" },
+    }
+    for _, case in ipairs(cases) do
+      local input, expected = unpack(case)
+      local output = util.url_escape(input)
+      assert.equals(expected, output)
+    end
+  end)
+
+  it("url_unescape", function()
+    local cases = {
+      { "foobar", "foobar" },
+      { "foo%20bar", "foo bar" },
+      { "%2Ffoo%2Fbar", "/foo/bar" },
+      { "foo%%bar", "foo%%bar" },
+    }
+    for _, case in ipairs(cases) do
+      local input, expected = unpack(case)
+      local output = util.url_unescape(input)
+      assert.equals(expected, output)
+    end
+  end)
+end)

From ab887d926c2665a708fbe9e6c4654042cc5f4c60 Mon Sep 17 00:00:00 2001
From: Shihua Zeng <76579810+Bekaboo@users.noreply.github.com>
Date: Thu, 20 Mar 2025 19:40:24 -0400
Subject: [PATCH 167/206] fix: indexing nil when env vars does not exist (#601)

---
 lua/oil/clipboard.lua | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/lua/oil/clipboard.lua b/lua/oil/clipboard.lua
index 32bcfdb..5a17850 100644
--- a/lua/oil/clipboard.lua
+++ b/lua/oil/clipboard.lua
@@ -10,7 +10,11 @@ local M = {}
 
 ---@return "wayland"|"x11"|nil
 local function get_linux_session_type()
-  local xdg_session_type = vim.env.XDG_SESSION_TYPE:lower()
+  local xdg_session_type = vim.env.XDG_SESSION_TYPE
+  if not xdg_session_type then
+    return
+  end
+  xdg_session_type = xdg_session_type:lower()
   if xdg_session_type:find("x11") then
     return "x11"
   elseif xdg_session_type:find("wayland") then
@@ -23,7 +27,9 @@ end
 ---@return boolean
 local function is_linux_desktop_gnome()
   local cur_desktop = vim.env.XDG_CURRENT_DESKTOP
-  local idx = vim.env.XDG_SESSION_DESKTOP:lower():find("gnome") or cur_desktop:lower():find("gnome")
+  local session_desktop = vim.env.XDG_SESSION_DESKTOP
+  local idx = session_desktop and session_desktop:lower():find("gnome")
+    or cur_desktop and cur_desktop:lower():find("gnome")
   return idx ~= nil or cur_desktop == "X-Cinnamon" or cur_desktop == "XFCE"
 end
 

From ba1f50a9a81f65c07af584065ab9a5ad2a9e5fe0 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sun, 30 Mar 2025 15:14:11 -0700
Subject: [PATCH 168/206] fix: file time column escapes ()[] chars in parser
 (#603)

---
 lua/oil/adapters/files.lua | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 8fb1835..28e647b 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -183,7 +183,17 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
         -- Replace placeholders with a pattern that matches non-space characters (e.g. %H -> %S+)
         -- and whitespace with a pattern that matches any amount of whitespace
         -- e.g. "%b %d %Y" -> "%S+%s+%S+%s+%S+"
-        pattern = fmt:gsub("%%.", "%%S+"):gsub("%s+", "%%s+")
+        pattern = fmt
+          :gsub("%%.", "%%S+")
+          :gsub("%s+", "%%s+")
+          -- escape `()[]` because those are special characters in Lua patterns
+          :gsub(
+            "%(",
+            "%%("
+          )
+          :gsub("%)", "%%)")
+          :gsub("%[", "%%[")
+          :gsub("%]", "%%]")
       else
         pattern = "%S+%s+%d+%s+%d%d:?%d%d"
       end

From 5b38bfe2795ad1b4d0f16000cacd8faa742e4850 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sun, 30 Mar 2025 21:56:41 -0700
Subject: [PATCH 169/206] doc: fix typecheck errors in nvim 0.11

---
 lua/oil/actions.lua                | 15 ++++++++++--
 lua/oil/adapters/trash/windows.lua |  9 ++++----
 lua/oil/lsp/workspace.lua          | 37 ++++++++++++++++++++++--------
 lua/oil/util.lua                   | 13 ++++++-----
 4 files changed, 53 insertions(+), 21 deletions(-)

diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua
index 6315b68..6dc56b8 100644
--- a/lua/oil/actions.lua
+++ b/lua/oil/actions.lua
@@ -229,13 +229,24 @@ M.open_terminal = {
       assert(dir, "Oil buffer with files adapter must have current directory")
       local bufnr = vim.api.nvim_create_buf(false, true)
       vim.api.nvim_set_current_buf(bufnr)
-      vim.fn.termopen(vim.o.shell, { cwd = dir })
+      if vim.fn.has("nvim-0.11") == 1 then
+        vim.fn.jobstart(vim.o.shell, { cwd = dir, term = true })
+      else
+        ---@diagnostic disable-next-line: deprecated
+        vim.fn.termopen(vim.o.shell, { cwd = dir })
+      end
     elseif adapter.name == "ssh" then
       local bufnr = vim.api.nvim_create_buf(false, true)
       vim.api.nvim_set_current_buf(bufnr)
       local url = require("oil.adapters.ssh").parse_url(bufname)
       local cmd = require("oil.adapters.ssh.connection").create_ssh_command(url)
-      local term_id = vim.fn.termopen(cmd)
+      local term_id
+      if vim.fn.has("nvim-0.11") == 1 then
+        term_id = vim.fn.jobstart(cmd, { term = true })
+      else
+        ---@diagnostic disable-next-line: deprecated
+        term_id = vim.fn.termopen(cmd)
+      end
       if term_id then
         vim.api.nvim_chan_send(term_id, string.format("cd %s\n", url.path))
       end
diff --git a/lua/oil/adapters/trash/windows.lua b/lua/oil/adapters/trash/windows.lua
index 4605567..dba1d04 100644
--- a/lua/oil/adapters/trash/windows.lua
+++ b/lua/oil/adapters/trash/windows.lua
@@ -37,10 +37,10 @@ local win_addslash = function(path)
 end
 
 ---@class oil.WindowsTrashInfo
----@field trash_file string?
----@field original_path string?
----@field deletion_date string?
----@field info_file string?
+---@field trash_file string
+---@field original_path string
+---@field deletion_date integer
+---@field info_file? string
 
 ---@param url string
 ---@param column_defs string[]
@@ -96,6 +96,7 @@ M.list = function(url, column_defs, cb)
           end
           cache_entry[FIELD_META] = {
             stat = nil,
+            ---@type oil.WindowsTrashInfo
             trash_info = {
               trash_file = entry.Path,
               original_path = entry.OriginalPath,
diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua
index ac8e180..9bacb13 100644
--- a/lua/oil/lsp/workspace.lua
+++ b/lua/oil/lsp/workspace.lua
@@ -167,8 +167,13 @@ local function will_file_operation(method, capability_name, files, options)
           }
         end, matching_files),
       }
-      ---@diagnostic disable-next-line: invisible
-      local result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0)
+      local result, err
+      if vim.fn.has("nvim-0.11") == 1 then
+        result, err = client:request_sync(method, params, options.timeout_ms or 1000, 0)
+      else
+        ---@diagnostic disable-next-line: param-type-mismatch
+        result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0)
+      end
       if result and result.result then
         if options.apply_edits ~= false then
           vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding)
@@ -204,8 +209,12 @@ local function did_file_operation(method, capability_name, files)
           }
         end, matching_files),
       }
-      ---@diagnostic disable-next-line: invisible
-      client.notify(method, params)
+      if vim.fn.has("nvim-0.11") == 1 then
+        client:notify(method, params)
+      else
+        ---@diagnostic disable-next-line: param-type-mismatch
+        client.notify(method, params)
+      end
     end
   end
 end
@@ -280,9 +289,15 @@ function M.will_rename_files(files, options)
           }
         end, matching_files),
       }
-      local result, err =
-        ---@diagnostic disable-next-line: invisible
-        client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
+      local result, err
+      if vim.fn.has("nvim-0.11") == 1 then
+        result, err =
+          client:request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
+      else
+        result, err =
+          ---@diagnostic disable-next-line: param-type-mismatch
+          client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
+      end
       if result and result.result then
         if options.apply_edits ~= false then
           vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding)
@@ -313,8 +328,12 @@ function M.did_rename_files(files)
           }
         end, matching_files),
       }
-      ---@diagnostic disable-next-line: invisible
-      client.notify(ms.workspace_didRenameFiles, params)
+      if vim.fn.has("nvim-0.11") == 1 then
+        client:notify(ms.workspace_didRenameFiles, params)
+      else
+        ---@diagnostic disable-next-line: param-type-mismatch
+        client.notify(ms.workspace_didRenameFiles, params)
+      end
     end
   end
 end
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 0ffebfc..ef7cb01 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -363,7 +363,12 @@ M.set_highlights = function(bufnr, highlights)
   local ns = vim.api.nvim_create_namespace("Oil")
   vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
   for _, hl in ipairs(highlights) do
-    vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl))
+    local group, line, col_start, col_end = unpack(hl)
+    vim.api.nvim_buf_set_extmark(bufnr, ns, line, col_start, {
+      end_col = col_end,
+      hl_group = group,
+      strict = false,
+    })
   end
 end
 
@@ -638,11 +643,7 @@ M.render_text = function(bufnr, text, opts)
   pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)
   vim.bo[bufnr].modifiable = false
   vim.bo[bufnr].modified = false
-  local ns = vim.api.nvim_create_namespace("Oil")
-  vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
-  for _, hl in ipairs(highlights) do
-    vim.api.nvim_buf_add_highlight(bufnr, ns, unpack(hl))
-  end
+  M.set_highlights(bufnr, highlights)
 end
 
 ---Run a function in the context of a full-editor window

From 302bbaceeafc690e6419e0c8296e804d60cb9446 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sun, 30 Mar 2025 21:58:22 -0700
Subject: [PATCH 170/206] ci: run tests against nvim 0.11

---
 .github/workflows/tests.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bdcc4c3..6d0945c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -52,8 +52,8 @@ jobs:
         include:
           - nvim_tag: v0.8.3
           - nvim_tag: v0.9.4
-          - nvim_tag: v0.10.0
           - nvim_tag: v0.10.4
+          - nvim_tag: v0.11.0
 
     name: Run tests
     runs-on: ubuntu-22.04

From 685cdb4ffa74473d75a1b97451f8654ceeab0f4a Mon Sep 17 00:00:00 2001
From: Alexandros Alexiou 
Date: Sun, 20 Apr 2025 20:35:57 +0300
Subject: [PATCH 171/206] fix: prevent E565 error when opening directories with
 nvim . (#608)

---
 lua/oil/util.lua | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index ef7cb01..3e70beb 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -174,8 +174,10 @@ M.rename_buffer = function(src_bufnr, dest_buf_name)
     -- This will fail if the dest buf name already exists
     local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name)
     if ok then
-      -- Renaming the buffer creates a new buffer with the old name. Find it and delete it.
-      vim.api.nvim_buf_delete(vim.fn.bufadd(bufname), {})
+      -- Renaming the buffer creates a new buffer with the old name.
+      -- Find it and try to delete it, but don't if the buffer is in a context
+      -- where Neovim doesn't allow buffer modifications.
+      pcall(vim.api.nvim_buf_delete, vim.fn.bufadd(bufname), {})
       if altbuf and vim.api.nvim_buf_is_valid(altbuf) then
         vim.fn.setreg("#", altbuf)
       end

From 35f7f000f4eee0ef43c9a629c164541251a2364d Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sun, 1 Jun 2025 10:58:43 -0700
Subject: [PATCH 172/206] doc: add a mention for third-party extension plugins

---
 README.md | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/README.md b/README.md
index ec3355b..4530065 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94
 - [Options](#options)
 - [Adapters](#adapters)
 - [Recipes](#recipes)
+- [Third-party extensions](#third-party-extensions)
 - [API](#api)
 - [FAQ](#faq)
 
@@ -360,6 +361,13 @@ Note that at the moment the ssh adapter does not support Windows machines, and i
 - [Show CWD in the winbar](doc/recipes.md#show-cwd-in-the-winbar)
 - [Hide gitignored files and show git tracked hidden files](doc/recipes.md#hide-gitignored-files-and-show-git-tracked-hidden-files)
 
+## Third-party extensions
+
+These are plugins maintained by other authors that extend the functionality of oil.nvim.
+
+- [oil-git-status.nvim](https://github.com/refractalize/oil-git-status.nvim) - Shows git status of files in statuscolumn
+- [oil-lsp-diagnostics.nvim](https://github.com/JezerM/oil-lsp-diagnostics.nvim) - Shows LSP diagnostics indicator as virtual text
+
 ## API
 
 

From 5b6068aad7d2057dd399fac73b7fb2cdf23ccd6e Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sun, 1 Jun 2025 11:02:23 -0700
Subject: [PATCH 173/206] fix: clean up empty buffer when opening in new tab
 (#616)

---
 lua/oil/init.lua | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 2c10b10..992c574 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -752,6 +752,8 @@ M.select = function(opts, callback)
       local cmd = "buffer"
       if opts.tab then
         vim.cmd.tabnew({ mods = mods })
+        -- Make sure the new buffer from tabnew gets cleaned up
+        vim.bo.bufhidden = "wipe"
       elseif opts.split then
         cmd = "sbuffer"
       end

From 08c2bce8b00fd780fb7999dbffdf7cd174e896fb Mon Sep 17 00:00:00 2001
From: kaerum <84108874+kaerumm@users.noreply.github.com>
Date: Wed, 4 Jun 2025 19:40:16 -0300
Subject: [PATCH 174/206] fix: glob formatting on windows in neovim nightly
 (#631)

* fix: makes workaround conditional as it is no longer needed for 0.12

* fix: formatted with proper stylua version
---
 lua/oil/lsp/workspace.lua | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua
index 9bacb13..d113b13 100644
--- a/lua/oil/lsp/workspace.lua
+++ b/lua/oil/lsp/workspace.lua
@@ -68,7 +68,8 @@ local function get_matching_paths(client, filters, paths)
       end
 
       -- Some language servers use forward slashes as path separators on Windows (LuaLS)
-      if fs.is_windows then
+      -- We no longer need this after 0.12: https://github.com/neovim/neovim/commit/322a6d305d088420b23071c227af07b7c1beb41a
+      if vim.fn.has("nvim-0.12") == 0 and fs.is_windows then
         glob = glob:gsub("/", "\\")
       end
 

From 1498d2fccff75a1321e92e4aa03ff220f87cc27e Mon Sep 17 00:00:00 2001
From: jiz4oh <41264693+jiz4oh@users.noreply.github.com>
Date: Tue, 1 Jul 2025 07:54:22 +0800
Subject: [PATCH 175/206] fix: ssh adapter supports iso8601 dates (#635)

* fix: add iso8601 format compatibility

* Update sshfs.lua

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 lua/oil/adapters/ssh/sshfs.lua | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/lua/oil/adapters/ssh/sshfs.lua b/lua/oil/adapters/ssh/sshfs.lua
index 341834c..0dcc169 100644
--- a/lua/oil/adapters/ssh/sshfs.lua
+++ b/lua/oil/adapters/ssh/sshfs.lua
@@ -42,10 +42,17 @@ local function parse_ls_line(line)
   local name, size, date, major, minor
   if typechar == "c" or typechar == "b" then
     major, minor, date, name = rem:match("^(%d+)%s*,%s*(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)")
+    if name == nil then
+      major, minor, date, name =
+        rem:match("^(%d+)%s*,%s*(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
+    end
     meta.major = tonumber(major)
     meta.minor = tonumber(minor)
   else
     size, date, name = rem:match("^(%d+)%s+(%S+%s+%d+%s+%d%d:?%d%d)%s+(.*)")
+    if name == nil then
+      size, date, name = rem:match("^(%d+)%s+(%d+%-%d+%-%d+%s+%d%d:?%d%d)%s+(.*)")
+    end
     meta.size = tonumber(size)
   end
   meta.iso_modified_date = date

From 3b7c74798e699633d602823aefd9a4e4e36c02a8 Mon Sep 17 00:00:00 2001
From: Ben O'Mahony 
Date: Wed, 2 Jul 2025 01:43:43 +0100
Subject: [PATCH 176/206] doc: add a mention to third party extension
 oil-git.nvim (#640)

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index 4530065..7f9d317 100644
--- a/README.md
+++ b/README.md
@@ -366,6 +366,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i
 These are plugins maintained by other authors that extend the functionality of oil.nvim.
 
 - [oil-git-status.nvim](https://github.com/refractalize/oil-git-status.nvim) - Shows git status of files in statuscolumn
+- [oil-git.nvim](https://github.com/benomahony/oil-git.nvim) - Shows git status of files with colour and symbols
 - [oil-lsp-diagnostics.nvim](https://github.com/JezerM/oil-lsp-diagnostics.nvim) - Shows LSP diagnostics indicator as virtual text
 
 ## API

From bbad9a76b2617ce1221d49619e4e4b659b3c61fc Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 2 Jul 2025 09:18:23 -0700
Subject: [PATCH 177/206] fix: scratch preview method (#628)

---
 lua/oil/util.lua | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 3e70beb..d000071 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -965,8 +965,12 @@ M.read_file_to_scratch_buffer = function(path, preview_method)
   vim.bo[bufnr].bufhidden = "wipe"
   vim.bo[bufnr].buftype = "nofile"
 
-  local max_lines = preview_method == "fast_scratch" and vim.o.lines or nil
-  local has_lines, read_res = pcall(vim.fn.readfile, path, "", max_lines)
+  local has_lines, read_res
+  if preview_method == "fast_scratch" then
+    has_lines, read_res = pcall(vim.fn.readfile, path, "", vim.o.lines)
+  else
+    has_lines, read_res = pcall(vim.fn.readfile, path)
+  end
   local lines = has_lines and vim.split(table.concat(read_res, "\n"), "\n") or {}
 
   local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines)

From 07f80ad645895af849a597d1cac897059d89b686 Mon Sep 17 00:00:00 2001
From: XeroOl 
Date: Wed, 20 Aug 2025 21:22:30 -0400
Subject: [PATCH 178/206] fix: support natural ordering for numbers with >12
 digits (#652)

* fix: support natural ordering for numbers with >12 digits

Changes the column ordering code when `view_options.natural_order`
is enabled, so that it can support larger numbers.

The previous 12-digit padding approach breaks for numbers above 12
digits.

This length-prefixed approach can scale to much higher numbers.
I picked %03 (padding 3 digits) because most filesystems don't allow
more than 255 bytes in a path segment, and "255" is 3 digits long.

* add memoization to natural order sorting

* remove call to unpack
---
 lua/oil/columns.lua | 20 +++++++++++---------
 lua/oil/view.lua    |  2 +-
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua
index cc3b445..d531a7c 100644
--- a/lua/oil/columns.lua
+++ b/lua/oil/columns.lua
@@ -228,8 +228,8 @@ M.register("type", {
   end,
 })
 
-local function pad_number(int)
-  return string.format("%012d", int)
+local function adjust_number(int)
+  return string.format("%03d%s", #int, int)
 end
 
 M.register("name", {
@@ -256,14 +256,16 @@ M.register("name", {
         end
       end
     else
-      if config.view_options.case_insensitive then
-        return function(entry)
-          return entry[FIELD_NAME]:gsub("%d+", pad_number):lower()
-        end
-      else
-        return function(entry)
-          return entry[FIELD_NAME]:gsub("%d+", pad_number)
+      local memo = {}
+      return function(entry)
+        if memo[entry] == nil then
+          local name = entry[FIELD_NAME]:gsub("0*(%d+)", adjust_number)
+          if config.view_options.case_insensitive then
+            name = name:lower()
+          end
+          memo[entry] = name
         end
+        return memo[entry]
       end
     end
   end,
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index c2cb076..e4145ad 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -587,7 +587,7 @@ local function get_sort_function(adapter, num_entries)
   end
   return function(a, b)
     for _, sort_fn in ipairs(idx_funs) do
-      local get_sort_value, order = unpack(sort_fn)
+      local get_sort_value, order = sort_fn[1], sort_fn[2]
       local a_val = get_sort_value(a)
       local b_val = get_sort_value(b)
       if a_val ~= b_val then

From 919e155fdf38e9148cdb5304faaaf53c20d703ea Mon Sep 17 00:00:00 2001
From: Random Dude 
Date: Sat, 27 Sep 2025 21:18:27 +0530
Subject: [PATCH 179/206] doc: mini.nvim has moved to new github organization
 (#663)

---
 README.md | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 7f9d317..04dc680 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ https://user-images.githubusercontent.com/506791/209727111-6b4a11f4-634a-4efa-94
 
 - Neovim 0.8+
 - Icon provider plugin (optional)
-  - [mini.icons](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-icons.md) for file and folder icons
+  - [mini.icons](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-icons.md) for file and folder icons
   - [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons
 
 ## Installation
@@ -39,7 +39,7 @@ oil.nvim supports all the usual plugin managers
   ---@type oil.SetupOpts
   opts = {},
   -- Optional dependencies
-  dependencies = { { "echasnovski/mini.icons", opts = {} } },
+  dependencies = { { "nvim-mini/mini.icons", opts = {} } },
   -- dependencies = { "nvim-tree/nvim-web-devicons" }, -- use if you prefer nvim-web-devicons
   -- Lazy loading is not recommended because it is very tricky to make it work correctly in all situations.
   lazy = false,
@@ -409,7 +409,7 @@ Plus, I think it's pretty slick ;)
 
 - You like to use a netrw-like view to browse directories (as opposed to a file tree)
 - AND you want to be able to edit your filesystem like a buffer
-- AND you want to perform cross-directory actions. AFAIK there is no other plugin that does this. (update: [mini.files](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-files.md) also offers this functionality)
+- AND you want to perform cross-directory actions. AFAIK there is no other plugin that does this. (update: [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md) also offers this functionality)
 
 If you don't need those features specifically, check out the alternatives listed below
 
@@ -425,7 +425,7 @@ If you don't need those features specifically, check out the alternatives listed
 
 **A:**
 
-- [mini.files](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-files.md): A newer plugin that also supports cross-directory filesystem-as-buffer edits. It utilizes a unique column view.
+- [mini.files](https://github.com/nvim-mini/mini.nvim/blob/main/readmes/mini-files.md): A newer plugin that also supports cross-directory filesystem-as-buffer edits. It utilizes a unique column view.
 - [vim-vinegar](https://github.com/tpope/vim-vinegar): The granddaddy. This made me fall in love with single-directory file browsing. I stopped using it when I encountered netrw bugs and performance issues.
 - [defx.nvim](https://github.com/Shougo/defx.nvim): What I switched to after vim-vinegar. Much more flexible and performant, but requires python and the API is a little hard to work with.
 - [dirbuf.nvim](https://github.com/elihunter173/dirbuf.nvim): The first plugin I encountered that let you edit the filesystem like a buffer. Never used it because it [can't do cross-directory edits](https://github.com/elihunter173/dirbuf.nvim/issues/7).

From 200df01e4b92d0010a3bfbde92b91a4ef5a5c9db Mon Sep 17 00:00:00 2001
From: Sebastian Lyng Johansen 
Date: Wed, 15 Oct 2025 07:30:41 +0200
Subject: [PATCH 180/206] fix: change default border config to nil (#643)

Neovim 0.11 introduced the winborder option, which serves the same purpose. By defaulting the border to nil, we will use whatever value the user has configured with winborder.

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 README.md          | 10 +++++-----
 doc/oil.txt        | 10 +++++-----
 lua/oil/config.lua | 21 ++++++++++++++++-----
 3 files changed, 26 insertions(+), 15 deletions(-)

diff --git a/README.md b/README.md
index 04dc680..adc6dca 100644
--- a/README.md
+++ b/README.md
@@ -262,7 +262,7 @@ require("oil").setup({
     -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
     max_width = 0,
     max_height = 0,
-    border = "rounded",
+    border = nil,
     win_options = {
       winblend = 0,
     },
@@ -307,7 +307,7 @@ require("oil").setup({
     min_height = { 5, 0.1 },
     -- optionally define an integer/float for the exact height of the preview window
     height = nil,
-    border = "rounded",
+    border = nil,
     win_options = {
       winblend = 0,
     },
@@ -320,7 +320,7 @@ require("oil").setup({
     max_height = { 10, 0.9 },
     min_height = { 5, 0.1 },
     height = nil,
-    border = "rounded",
+    border = nil,
     minimized_border = "none",
     win_options = {
       winblend = 0,
@@ -328,11 +328,11 @@ require("oil").setup({
   },
   -- Configuration for the floating SSH window
   ssh = {
-    border = "rounded",
+    border = nil,
   },
   -- Configuration for the floating keymaps help window
   keymaps_help = {
-    border = "rounded",
+    border = nil,
   },
 })
 ```
diff --git a/doc/oil.txt b/doc/oil.txt
index fdc7ca5..7086ffe 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -144,7 +144,7 @@ CONFIG                                                                *oil-confi
         -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
         max_width = 0,
         max_height = 0,
-        border = "rounded",
+        border = nil,
         win_options = {
           winblend = 0,
         },
@@ -189,7 +189,7 @@ CONFIG                                                                *oil-confi
         min_height = { 5, 0.1 },
         -- optionally define an integer/float for the exact height of the preview window
         height = nil,
-        border = "rounded",
+        border = nil,
         win_options = {
           winblend = 0,
         },
@@ -202,7 +202,7 @@ CONFIG                                                                *oil-confi
         max_height = { 10, 0.9 },
         min_height = { 5, 0.1 },
         height = nil,
-        border = "rounded",
+        border = nil,
         minimized_border = "none",
         win_options = {
           winblend = 0,
@@ -210,11 +210,11 @@ CONFIG                                                                *oil-confi
       },
       -- Configuration for the floating SSH window
       ssh = {
-        border = "rounded",
+        border = nil,
       },
       -- Configuration for the floating keymaps help window
       keymaps_help = {
-        border = "rounded",
+        border = nil,
       },
     })
 <
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 6797cc8..7b4e97c 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -127,7 +127,7 @@ local default_config = {
     -- max_width and max_height can be integers or a float between 0 and 1 (e.g. 0.4 for 40%)
     max_width = 0,
     max_height = 0,
-    border = "rounded",
+    border = nil,
     win_options = {
       winblend = 0,
     },
@@ -172,7 +172,7 @@ local default_config = {
     min_height = { 5, 0.1 },
     -- optionally define an integer/float for the exact height of the preview window
     height = nil,
-    border = "rounded",
+    border = nil,
     win_options = {
       winblend = 0,
     },
@@ -185,7 +185,7 @@ local default_config = {
     max_height = { 10, 0.9 },
     min_height = { 5, 0.1 },
     height = nil,
-    border = "rounded",
+    border = nil,
     minimized_border = "none",
     win_options = {
       winblend = 0,
@@ -193,11 +193,11 @@ local default_config = {
   },
   -- Configuration for the floating SSH window
   ssh = {
-    border = "rounded",
+    border = nil,
   },
   -- Configuration for the floating keymaps help window
   keymaps_help = {
-    border = "rounded",
+    border = nil,
   },
 }
 
@@ -412,6 +412,17 @@ M.setup = function(opts)
     end
   end
 
+  -- Backwards compatibility for old versions that don't support winborder
+  if vim.fn.has("nvim-0.11") == 0 then
+    new_conf = vim.tbl_deep_extend("keep", new_conf, {
+      float = { border = "rounded" },
+      confirmation = { border = "rounded" },
+      progress = { border = "rounded" },
+      ssh = { border = "rounded" },
+      keymaps_help = { border = "rounded" },
+    })
+  end
+
   -- Backwards compatibility. We renamed the 'preview' window config to be called 'confirmation'.
   if opts.preview and not opts.confirmation then
     new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation)

From dfb09e87bfb6d0d4d7896211dc0f18a40747875d Mon Sep 17 00:00:00 2001
From: John Winston <59228178+winston0410@users.noreply.github.com>
Date: Wed, 15 Oct 2025 18:03:09 +0100
Subject: [PATCH 181/206] feat: add callback for handling buffer opening (#638)

---
 lua/oil/init.lua | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 992c574..4d4bd23 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -618,6 +618,7 @@ end
 ---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
 ---@field tab? boolean Open the buffer in a new tab
 ---@field close? boolean Close the original oil buffer once selection is made
+---@field handle_buffer_callback? fun(buf_id: integer) If defined, all other buffer related options here would be ignored. This callback allows you to take over the process of opening the buffer yourself.
 
 ---Select the entry under the cursor
 ---@param opts nil|oil.SelectOpts
@@ -757,15 +758,19 @@ M.select = function(opts, callback)
       elseif opts.split then
         cmd = "sbuffer"
       end
-      ---@diagnostic disable-next-line: param-type-mismatch
-      local ok, err = pcall(vim.cmd, {
-        cmd = cmd,
-        args = { filebufnr },
-        mods = mods,
-      })
-      -- Ignore swapfile errors
-      if not ok and err and not err:match("^Vim:E325:") then
-        vim.api.nvim_echo({ { err, "Error" } }, true, {})
+      if opts.handle_buffer_callback ~= nil then
+        opts.handle_buffer_callback(filebufnr)
+      else
+        ---@diagnostic disable-next-line: param-type-mismatch
+        local ok, err = pcall(vim.cmd, {
+          cmd = cmd,
+          args = { filebufnr },
+          mods = mods,
+        })
+        -- Ignore swapfile errors
+        if not ok and err and not err:match("^Vim:E325:") then
+          vim.api.nvim_echo({ { err, "Error" } }, true, {})
+        end
       end
 
       open_next_entry(cb)

From 64dbcaa91d6e378e9c84fcb24756f6f452f7b49d Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Wed, 15 Oct 2025 17:03:28 +0000
Subject: [PATCH 182/206] [docgen] Update docs skip-checks: true

---
 doc/api.md  | 19 ++++++++++---------
 doc/oil.txt |  4 ++++
 2 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/doc/api.md b/doc/api.md
index 3e6a3e6..f4577c4 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -159,15 +159,16 @@ Preview the entry under the cursor in a split
 `select(opts, callback)` \
 Select the entry under the cursor
 
-| Param       | Type                                                    | Desc                                                 |
-| ----------- | ------------------------------------------------------- | ---------------------------------------------------- |
-| opts        | `nil\|oil.SelectOpts`                                   |                                                      |
-| >vertical   | `nil\|boolean`                                          | Open the buffer in a vertical split                  |
-| >horizontal | `nil\|boolean`                                          | Open the buffer in a horizontal split                |
-| >split      | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier                                       |
-| >tab        | `nil\|boolean`                                          | Open the buffer in a new tab                         |
-| >close      | `nil\|boolean`                                          | Close the original oil buffer once selection is made |
-| callback    | `nil\|fun(err: nil\|string)`                            | Called once all entries have been opened             |
+| Param                   | Type                                                    | Desc                                                                                                                                                  |
+| ----------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
+| opts                    | `nil\|oil.SelectOpts`                                   |                                                                                                                                                       |
+| >vertical               | `nil\|boolean`                                          | Open the buffer in a vertical split                                                                                                                   |
+| >horizontal             | `nil\|boolean`                                          | Open the buffer in a horizontal split                                                                                                                 |
+| >split                  | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier                                                                                                                                        |
+| >tab                    | `nil\|boolean`                                          | Open the buffer in a new tab                                                                                                                          |
+| >close                  | `nil\|boolean`                                          | Close the original oil buffer once selection is made                                                                                                  |
+| >handle_buffer_callback | `nil\|fun(buf_id: integer)`                             | If defined, all other buffer related options here would be ignored. This callback allows you to take over the process of opening the buffer yourself. |
+| callback                | `nil\|fun(err: nil\|string)`                            | Called once all entries have been opened                                                                                                              |
 
 ## save(opts, cb)
 
diff --git a/doc/oil.txt b/doc/oil.txt
index 7086ffe..35c9da2 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -373,6 +373,10 @@ select({opts}, {callback})                                            *oil.selec
           {tab}        `nil|boolean` Open the buffer in a new tab
           {close}      `nil|boolean` Close the original oil buffer once
                        selection is made
+          {handle_buffer_callback} `nil|fun(buf_id: integer)` If defined, all
+                       other buffer related options here would be ignored. This
+                       callback allows you to take over the process of opening
+                       the buffer yourself.
       {callback} `nil|fun(err: nil|string)` Called once all entries have been
                  opened
 

From f55ebb007946b57561e7c337fdb1fd4d4622df33 Mon Sep 17 00:00:00 2001
From: Steve Walker <65963536+stevalkr@users.noreply.github.com>
Date: Thu, 16 Oct 2025 01:36:37 +0800
Subject: [PATCH 183/206] feat(clipboard): pasting from system clipboard can
 delete original (cut) (#649)

* feat: cut_from_system_clipboard

* refactor: shuffle some code around

---------

Co-authored-by: Steven Arcangeli 
---
 doc/oil.txt           |  3 +++
 lua/oil/actions.lua   | 10 ++++++++--
 lua/oil/clipboard.lua | 44 +++++++++++++++++++++++++++++++++++++++----
 3 files changed, 51 insertions(+), 6 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index 35c9da2..7bcd29e 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -575,6 +575,9 @@ parent                                                            *actions.paren
 paste_from_system_clipboard                  *actions.paste_from_system_clipboard*
     Paste the system clipboard into the current oil directory
 
+    Parameters:
+      {delete_original} `boolean` Delete the original file after copying
+
 preview                                                          *actions.preview*
     Open the entry under the cursor in a preview window, or close the preview
     window if already open
diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua
index 6dc56b8..ead4f51 100644
--- a/lua/oil/actions.lua
+++ b/lua/oil/actions.lua
@@ -438,9 +438,15 @@ M.copy_to_system_clipboard = {
 
 M.paste_from_system_clipboard = {
   desc = "Paste the system clipboard into the current oil directory",
-  callback = function()
-    require("oil.clipboard").paste_from_system_clipboard()
+  callback = function(opts)
+    require("oil.clipboard").paste_from_system_clipboard(opts and opts.delete_original)
   end,
+  parameters = {
+    delete_original = {
+      type = "boolean",
+      desc = "Delete the original file after copying",
+    },
+  },
 }
 
 M.open_cmdline_dir = {
diff --git a/lua/oil/clipboard.lua b/lua/oil/clipboard.lua
index 5a17850..04498f5 100644
--- a/lua/oil/clipboard.lua
+++ b/lua/oil/clipboard.lua
@@ -3,6 +3,7 @@ local columns = require("oil.columns")
 local config = require("oil.config")
 local fs = require("oil.fs")
 local oil = require("oil")
+local parser = require("oil.mutator.parser")
 local util = require("oil.util")
 local view = require("oil.view")
 
@@ -50,8 +51,31 @@ local function write_pasted(winid, entry, column_defs, adapter, bufnr)
   vim.api.nvim_buf_set_lines(bufnr, pos[1], pos[1], true, lines)
 end
 
+---@param parent_url string
+---@param entry oil.InternalEntry
+local function remove_entry_from_parent_buffer(parent_url, entry)
+  local bufnr = vim.fn.bufadd(parent_url)
+  assert(vim.api.nvim_buf_is_loaded(bufnr), "Expected parent buffer to be loaded during paste")
+  local adapter = assert(util.get_adapter(bufnr))
+  local column_defs = columns.get_supported_columns(adapter)
+  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
+  for i, line in ipairs(lines) do
+    local result = parser.parse_line(adapter, line, column_defs)
+    if result and result.entry == entry then
+      vim.api.nvim_buf_set_lines(bufnr, i - 1, i, false, {})
+      return
+    end
+  end
+  local exported = util.export_entry(entry)
+  vim.notify(
+    string.format("Error: could not delete original file '%s'", exported.name),
+    vim.log.levels.ERROR
+  )
+end
+
 ---@param paths string[]
-local function paste_paths(paths)
+---@param delete_original? boolean
+local function paste_paths(paths, delete_original)
   local bufnr = vim.api.nvim_get_current_buf()
   local scheme = "oil://"
   local adapter = assert(config.get_adapter_by_scheme(scheme))
@@ -61,6 +85,7 @@ local function paste_paths(paths)
   local parent_urls = {}
   local pending_paths = {}
 
+  -- Handle as many paths synchronously as possible
   for _, path in ipairs(paths) do
     -- Trim the trailing slash off directories
     if vim.endswith(path, "/") then
@@ -68,18 +93,24 @@ local function paste_paths(paths)
     end
 
     local ori_entry = cache.get_entry_by_url(scheme .. path)
+    local parent_url = util.addslash(scheme .. vim.fs.dirname(path))
     if ori_entry then
       write_pasted(winid, ori_entry, column_defs, adapter, bufnr)
+      if delete_original then
+        remove_entry_from_parent_buffer(parent_url, ori_entry)
+      end
     else
-      local parent_url = scheme .. vim.fs.dirname(path)
       parent_urls[parent_url] = true
       table.insert(pending_paths, path)
     end
   end
+
+  -- If all paths could be handled synchronously, we're done
   if #pending_paths == 0 then
     return
   end
 
+  -- Process the remaining paths by asynchronously loading them
   local cursor = vim.api.nvim_win_get_cursor(winid)
   local complete_loading = util.cb_collect(#vim.tbl_keys(parent_urls), function(err)
     if err then
@@ -92,6 +123,10 @@ local function paste_paths(paths)
         local ori_entry = cache.get_entry_by_url(scheme .. path)
         if ori_entry then
           write_pasted(winid, ori_entry, column_defs, adapter, bufnr)
+          if delete_original then
+            local parent_url = util.addslash(scheme .. vim.fs.dirname(path))
+            remove_entry_from_parent_buffer(parent_url, ori_entry)
+          end
         else
           vim.notify(
             string.format("The pasted file '%s' could not be found", path),
@@ -261,7 +296,8 @@ local function handle_paste_output_linux(lines)
   return ret
 end
 
-M.paste_from_system_clipboard = function()
+---@param delete_original? boolean Delete the source file after pasting
+M.paste_from_system_clipboard = function(delete_original)
   local dir = oil.get_current_dir()
   if not dir then
     return
@@ -324,7 +360,7 @@ M.paste_from_system_clipboard = function()
       elseif #paths == 0 then
         vim.notify("No valid files found in system clipboard", vim.log.levels.WARN)
       else
-        paste_paths(paths)
+        paste_paths(paths, delete_original)
       end
     end,
   })

From 71948729cda5fc1b761d6ae60ff774b5525f1d50 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Wed, 15 Oct 2025 10:42:52 -0700
Subject: [PATCH 184/206] lint: use more specific type for internal entries

---
 lua/oil/adapters/trash/freedesktop.lua | 8 ++++----
 lua/oil/adapters/trash/windows.lua     | 2 +-
 lua/oil/columns.lua                    | 2 +-
 lua/oil/constants.lua                  | 2 +-
 4 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/lua/oil/adapters/trash/freedesktop.lua b/lua/oil/adapters/trash/freedesktop.lua
index c669730..10c0749 100644
--- a/lua/oil/adapters/trash/freedesktop.lua
+++ b/lua/oil/adapters/trash/freedesktop.lua
@@ -447,7 +447,7 @@ M.render_action = function(action)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.TrashInfo
-    local trash_info = meta and meta.trash_info
+    local trash_info = assert(meta).trash_info
     local short_path = fs.shorten_path(trash_info.original_path)
     return string.format(" PURGE %s", short_path)
   elseif action.type == "move" then
@@ -561,7 +561,7 @@ M.perform_action = function(action, cb)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.TrashInfo
-    local trash_info = meta and meta.trash_info
+    local trash_info = assert(meta).trash_info
     purge(trash_info, cb)
   elseif action.type == "move" then
     local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
@@ -576,7 +576,7 @@ M.perform_action = function(action, cb)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META]
       ---@type oil.TrashInfo
-      local trash_info = meta and meta.trash_info
+      local trash_info = assert(meta).trash_info
       fs.recursive_move(action.entry_type, trash_info.trash_file, dest_path, function(err)
         if err then
           return cb(err)
@@ -607,7 +607,7 @@ M.perform_action = function(action, cb)
       local entry = assert(cache.get_entry_by_url(action.src_url))
       local meta = entry[FIELD_META]
       ---@type oil.TrashInfo
-      local trash_info = meta and meta.trash_info
+      local trash_info = assert(meta).trash_info
       fs.recursive_copy(action.entry_type, trash_info.trash_file, dest_path, cb)
     else
       error("Must be moving files into or out of trash")
diff --git a/lua/oil/adapters/trash/windows.lua b/lua/oil/adapters/trash/windows.lua
index dba1d04..f7634e1 100644
--- a/lua/oil/adapters/trash/windows.lua
+++ b/lua/oil/adapters/trash/windows.lua
@@ -266,7 +266,7 @@ M.render_action = function(action)
     local entry = assert(cache.get_entry_by_url(action.url))
     local meta = entry[FIELD_META]
     ---@type oil.WindowsTrashInfo
-    local trash_info = meta and meta.trash_info
+    local trash_info = assert(meta).trash_info
     local short_path = fs.shorten_path(trash_info.original_path)
     return string.format(" PURGE %s", short_path)
   elseif action.type == "move" then
diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua
index d531a7c..e3a4395 100644
--- a/lua/oil/columns.lua
+++ b/lua/oil/columns.lua
@@ -200,7 +200,7 @@ local function is_entry_directory(entry)
     return true
   elseif type == "link" then
     local meta = entry[FIELD_META]
-    return meta and meta.link_stat and meta.link_stat.type == "directory"
+    return (meta and meta.link_stat and meta.link_stat.type == "directory") == true
   else
     return false
   end
diff --git a/lua/oil/constants.lua b/lua/oil/constants.lua
index e1ef56b..3f5a38a 100644
--- a/lua/oil/constants.lua
+++ b/lua/oil/constants.lua
@@ -2,7 +2,7 @@ local M = {}
 
 ---Store entries as a list-like table for maximum space efficiency and retrieval speed.
 ---We use the constants below to index into the table.
----@alias oil.InternalEntry any[]
+---@alias oil.InternalEntry {[1]: integer, [2]: string, [3]: oil.EntryType, [4]: nil|table}
 
 -- Indexes into oil.InternalEntry
 M.FIELD_ID = 1

From 7e1cd7703ff2924d7038476dcbc04b950203b902 Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sun, 19 Oct 2025 15:39:22 -0700
Subject: [PATCH 185/206] fix: don't apply oil window options to non-directory
 oil buffers

---
 lua/oil/init.lua | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 4d4bd23..44945bb 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -1286,7 +1286,10 @@ M.setup = function(opts)
       local util = require("oil.util")
       local bufname = vim.api.nvim_buf_get_name(0)
       local scheme = util.parse_url(bufname)
-      if scheme and config.adapters[scheme] then
+      local is_oil_buf = scheme and config.adapters[scheme]
+      -- We want to filter out oil buffers that are not directories (i.e. ssh files)
+      local is_oil_dir_or_unknown = (vim.bo.filetype == "oil" or vim.bo.filetype == "")
+      if is_oil_buf and is_oil_dir_or_unknown then
         local view = require("oil.view")
         view.maybe_set_cursor()
         -- While we are in an oil buffer, set the alternate file to the buffer we were in prior to

From 01cb3a8ad7d5e8707041edc775af83dbf33838f4 Mon Sep 17 00:00:00 2001
From: Zijian <43778097+znxuz@users.noreply.github.com>
Date: Sat, 22 Nov 2025 02:45:13 +0100
Subject: [PATCH 186/206] fix: send_to_quickfix opens loclist when specified
 (#687)

* fix `send_to_quickfix` opening qf when the target is `loclist`

* fix indentation
---
 lua/oil/util.lua | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index d000071..894027e 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -823,11 +823,12 @@ M.send_to_quickfix = function(opts)
   local action = opts.action == "a" and "a" or "r"
   if opts.target == "loclist" then
     vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries })
+    vim.cmd.lopen()
   else
     vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries })
+    vim.cmd.copen()
   end
   vim.api.nvim_exec_autocmds("QuickFixCmdPost", {})
-  vim.cmd.copen()
 end
 
 ---@return boolean

From e5bd931edb93a48a3a282f43992c5495966f5ba9 Mon Sep 17 00:00:00 2001
From: Daniel Kongsgaard 
Date: Sun, 30 Nov 2025 21:41:37 +0100
Subject: [PATCH 187/206] feat: new adapter for S3 buckets (#677)

* Added s3 support

* Save work

* Various bug fixes

* Minor cleanup

* Minor bug fixes

* Fix typo

* Update following feedback + minor bug fix

* Fix CI

* Cleanup and remove bucket entry_type

* Make suggested changes

* Better aws existence check

* Fix typo

* refactor: don't bother caching aws executable status

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 README.md                    |  12 ++
 doc/oil.txt                  |   6 +-
 lua/oil/adapters/s3.lua      | 389 +++++++++++++++++++++++++++++++++++
 lua/oil/adapters/s3/s3fs.lua | 149 ++++++++++++++
 lua/oil/config.lua           |   9 +
 syntax/oil_preview.vim       |   4 +-
 6 files changed, 565 insertions(+), 4 deletions(-)
 create mode 100644 lua/oil/adapters/s3.lua
 create mode 100644 lua/oil/adapters/s3/s3fs.lua

diff --git a/README.md b/README.md
index adc6dca..deecf74 100644
--- a/README.md
+++ b/README.md
@@ -242,6 +242,8 @@ require("oil").setup({
   },
   -- Extra arguments to pass to SCP when moving/copying files over SSH
   extra_scp_args = {},
+  -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
+  extra_s3_args = {},
   -- EXPERIMENTAL support for performing file operations with git
   git = {
     -- Return true to automatically git add/mv/rm files
@@ -355,6 +357,16 @@ This may look familiar. In fact, this is the same url format that netrw uses.
 
 Note that at the moment the ssh adapter does not support Windows machines, and it requires the server to have a `/bin/sh` binary as well as standard unix commands (`ls`, `rm`, `mv`, `mkdir`, `chmod`, `cp`, `touch`, `ln`, `echo`).
 
+### S3
+
+This adapter allows you to browse files stored in aws s3. To use it, make sure `aws` is setup correctly and then simply open a buffer using the following name template:
+
+```
+nvim oil-s3://[bucket]/[path]
+```
+
+Note that older versions of Neovim don't support numbers in the url, so for Neovim 0.11 and older the url starts with `oil-sss`.
+
 ## Recipes
 
 - [Toggle file detail view](doc/recipes.md#toggle-file-detail-view)
diff --git a/doc/oil.txt b/doc/oil.txt
index 7bcd29e..fdc7ed1 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -124,6 +124,8 @@ CONFIG                                                                *oil-confi
       },
       -- Extra arguments to pass to SCP when moving/copying files over SSH
       extra_scp_args = {},
+      -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
+      extra_s3_args = {},
       -- EXPERIMENTAL support for performing file operations with git
       git = {
         -- Return true to automatically git add/mv/rm files
@@ -428,7 +430,7 @@ icon                                                                 *column-ico
                      the icon
 
 size                                                                 *column-size*
-    Adapters: files, ssh
+    Adapters: files, ssh, s3
     Sortable: this column can be used in view_props.sort
     The size of the file
 
@@ -476,7 +478,7 @@ atime                                                               *column-atim
       {format}    `string` Format string (see :help strftime)
 
 birthtime                                                       *column-birthtime*
-    Adapters: files
+    Adapters: files, s3
     Sortable: this column can be used in view_props.sort
     The time the file was created
 
diff --git a/lua/oil/adapters/s3.lua b/lua/oil/adapters/s3.lua
new file mode 100644
index 0000000..b7b5400
--- /dev/null
+++ b/lua/oil/adapters/s3.lua
@@ -0,0 +1,389 @@
+local config = require("oil.config")
+local constants = require("oil.constants")
+local files = require("oil.adapters.files")
+local fs = require("oil.fs")
+local loading = require("oil.loading")
+local pathutil = require("oil.pathutil")
+local s3fs = require("oil.adapters.s3.s3fs")
+local util = require("oil.util")
+local M = {}
+
+local FIELD_META = constants.FIELD_META
+
+---@class (exact) oil.s3Url
+---@field scheme string
+---@field bucket nil|string
+---@field path nil|string
+
+---@param oil_url string
+---@return oil.s3Url
+M.parse_url = function(oil_url)
+  local scheme, url = util.parse_url(oil_url)
+  assert(scheme and url, string.format("Malformed input url '%s'", oil_url))
+  local ret = { scheme = scheme }
+  local bucket, path = url:match("^([^/]+)/?(.*)$")
+  ret.bucket = bucket
+  ret.path = path ~= "" and path or nil
+  if not ret.bucket and ret.path then
+    error(string.format("Parsing error for s3 url: %s", oil_url))
+  end
+  ---@cast ret oil.s3Url
+  return ret
+end
+
+---@param url oil.s3Url
+---@return string
+local function url_to_str(url)
+  local pieces = { url.scheme }
+  if url.bucket then
+    assert(url.bucket ~= "")
+    table.insert(pieces, url.bucket)
+    table.insert(pieces, "/")
+  end
+  if url.path then
+    assert(url.path ~= "")
+    table.insert(pieces, url.path)
+  end
+  return table.concat(pieces, "")
+end
+
+---@param url oil.s3Url
+---@param is_folder boolean
+---@return string
+local function url_to_s3(url, is_folder)
+  local pieces = { "s3://" }
+  if url.bucket then
+    assert(url.bucket ~= "")
+    table.insert(pieces, url.bucket)
+    table.insert(pieces, "/")
+  end
+  if url.path then
+    assert(url.path ~= "")
+    table.insert(pieces, url.path)
+    if is_folder and not vim.endswith(url.path, "/") then
+      table.insert(pieces, "/")
+    end
+  end
+  return table.concat(pieces, "")
+end
+
+---@param url oil.s3Url
+---@return boolean
+local function is_bucket(url)
+  assert(url.bucket and url.bucket ~= "")
+  if url.path then
+    assert(url.path ~= "")
+    return false
+  end
+  return true
+end
+
+local s3_columns = {}
+s3_columns.size = {
+  render = function(entry, conf)
+    local meta = entry[FIELD_META]
+    if not meta or not meta.size then
+      return ""
+    elseif meta.size >= 1e9 then
+      return string.format("%.1fG", meta.size / 1e9)
+    elseif meta.size >= 1e6 then
+      return string.format("%.1fM", meta.size / 1e6)
+    elseif meta.size >= 1e3 then
+      return string.format("%.1fk", meta.size / 1e3)
+    else
+      return string.format("%d", meta.size)
+    end
+  end,
+
+  parse = function(line, conf)
+    return line:match("^(%d+%S*)%s+(.*)$")
+  end,
+
+  get_sort_value = function(entry)
+    local meta = entry[FIELD_META]
+    if meta and meta.size then
+      return meta.size
+    else
+      return 0
+    end
+  end,
+}
+
+s3_columns.birthtime = {
+  render = function(entry, conf)
+    local meta = entry[FIELD_META]
+    if not meta or not meta.date then
+      return ""
+    else
+      return meta.date
+    end
+  end,
+
+  parse = function(line, conf)
+    return line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$")
+  end,
+
+  get_sort_value = function(entry)
+    local meta = entry[FIELD_META]
+    if meta and meta.date then
+      local year, month, day, hour, min, sec =
+        meta.date:match("^(%d+)%-(%d+)%-(%d+)%s(%d+):(%d+):(%d+)$")
+      local time =
+        os.time({ year = year, month = month, day = day, hour = hour, min = min, sec = sec })
+      return time
+    else
+      return 0
+    end
+  end,
+}
+
+---@param name string
+---@return nil|oil.ColumnDefinition
+M.get_column = function(name)
+  return s3_columns[name]
+end
+
+---@param bufname string
+---@return string
+M.get_parent = function(bufname)
+  local res = M.parse_url(bufname)
+  if res.path then
+    assert(res.path ~= "")
+    local path = pathutil.parent(res.path)
+    res.path = path ~= "" and path or nil
+  else
+    res.bucket = nil
+  end
+  return url_to_str(res)
+end
+
+---@param url string
+---@param callback fun(url: string)
+M.normalize_url = function(url, callback)
+  local res = M.parse_url(url)
+  callback(url_to_str(res))
+end
+
+---@param url string
+---@param column_defs string[]
+---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
+M.list = function(url, column_defs, callback)
+  if vim.fn.executable("aws") ~= 1 then
+    callback("`aws` is not executable. Can you run `aws s3 ls`?")
+    return
+  end
+
+  local res = M.parse_url(url)
+  s3fs.list_dir(url, url_to_s3(res, true), callback)
+end
+
+---@param bufnr integer
+---@return boolean
+M.is_modifiable = function(bufnr)
+  -- default assumption is that everything is modifiable
+  return true
+end
+
+---@param action oil.Action
+---@return string
+M.render_action = function(action)
+  local is_folder = action.entry_type == "directory"
+  if action.type == "create" then
+    local res = M.parse_url(action.url)
+    local extra = is_bucket(res) and "BUCKET " or ""
+    return string.format("CREATE %s%s", extra, url_to_s3(res, is_folder))
+  elseif action.type == "delete" then
+    local res = M.parse_url(action.url)
+    local extra = is_bucket(res) and "BUCKET " or ""
+    return string.format("DELETE %s%s", extra, url_to_s3(res, is_folder))
+  elseif action.type == "move" or action.type == "copy" then
+    local src = action.src_url
+    local dest = action.dest_url
+    if config.get_adapter_by_scheme(src) ~= M then
+      local _, path = util.parse_url(src)
+      assert(path)
+      src = files.to_short_os_path(path, action.entry_type)
+      dest = url_to_s3(M.parse_url(dest), is_folder)
+    elseif config.get_adapter_by_scheme(dest) ~= M then
+      local _, path = util.parse_url(dest)
+      assert(path)
+      dest = files.to_short_os_path(path, action.entry_type)
+      src = url_to_s3(M.parse_url(src), is_folder)
+    end
+    return string.format("  %s %s -> %s", action.type:upper(), src, dest)
+  else
+    error(string.format("Bad action type: '%s'", action.type))
+  end
+end
+
+---@param action oil.Action
+---@param cb fun(err: nil|string)
+M.perform_action = function(action, cb)
+  local is_folder = action.entry_type == "directory"
+  if action.type == "create" then
+    local res = M.parse_url(action.url)
+    local bucket = is_bucket(res)
+
+    if action.entry_type == "directory" and bucket then
+      s3fs.mb(url_to_s3(res, true), cb)
+    elseif action.entry_type == "directory" or action.entry_type == "file" then
+      s3fs.touch(url_to_s3(res, is_folder), cb)
+    else
+      cb(string.format("Bad entry type on s3 create action: %s", action.entry_type))
+    end
+  elseif action.type == "delete" then
+    local res = M.parse_url(action.url)
+    local bucket = is_bucket(res)
+
+    if action.entry_type == "directory" and bucket then
+      s3fs.rb(url_to_s3(res, true), cb)
+    elseif action.entry_type == "directory" or action.entry_type == "file" then
+      s3fs.rm(url_to_s3(res, is_folder), is_folder, cb)
+    else
+      cb(string.format("Bad entry type on s3 delete action: %s", action.entry_type))
+    end
+  elseif action.type == "move" then
+    local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
+    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
+    if
+      (src_adapter ~= M and src_adapter ~= files) or (dest_adapter ~= M and dest_adapter ~= files)
+    then
+      cb(
+        string.format(
+          "We should never attempt to move from the %s adapter to the %s adapter.",
+          src_adapter.name,
+          dest_adapter.name
+        )
+      )
+    end
+
+    local src, _
+    if src_adapter == M then
+      local src_res = M.parse_url(action.src_url)
+      src = url_to_s3(src_res, is_folder)
+    else
+      _, src = util.parse_url(action.src_url)
+    end
+    assert(src)
+
+    local dest
+    if dest_adapter == M then
+      local dest_res = M.parse_url(action.dest_url)
+      dest = url_to_s3(dest_res, is_folder)
+    else
+      _, dest = util.parse_url(action.dest_url)
+    end
+    assert(dest)
+
+    s3fs.mv(src, dest, is_folder, cb)
+  elseif action.type == "copy" then
+    local src_adapter = assert(config.get_adapter_by_scheme(action.src_url))
+    local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url))
+    if
+      (src_adapter ~= M and src_adapter ~= files) or (dest_adapter ~= M and dest_adapter ~= files)
+    then
+      cb(
+        string.format(
+          "We should never attempt to copy from the %s adapter to the %s adapter.",
+          src_adapter.name,
+          dest_adapter.name
+        )
+      )
+    end
+
+    local src, _
+    if src_adapter == M then
+      local src_res = M.parse_url(action.src_url)
+      src = url_to_s3(src_res, is_folder)
+    else
+      _, src = util.parse_url(action.src_url)
+    end
+    assert(src)
+
+    local dest
+    if dest_adapter == M then
+      local dest_res = M.parse_url(action.dest_url)
+      dest = url_to_s3(dest_res, is_folder)
+    else
+      _, dest = util.parse_url(action.dest_url)
+    end
+    assert(dest)
+
+    s3fs.cp(src, dest, is_folder, cb)
+  else
+    cb(string.format("Bad action type: %s", action.type))
+  end
+end
+
+M.supported_cross_adapter_actions = { files = "move" }
+
+---@param bufnr integer
+M.read_file = function(bufnr)
+  loading.set_loading(bufnr, true)
+  local bufname = vim.api.nvim_buf_get_name(bufnr)
+  local url = M.parse_url(bufname)
+  local basename = pathutil.basename(bufname)
+  local cache_dir = vim.fn.stdpath("cache")
+  assert(type(cache_dir) == "string")
+  local tmpdir = fs.join(cache_dir, "oil")
+  fs.mkdirp(tmpdir)
+  local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXX"))
+  if fd then
+    vim.loop.fs_close(fd)
+  end
+  local tmp_bufnr = vim.fn.bufadd(tmpfile)
+
+  s3fs.cp(url_to_s3(url, false), tmpfile, false, function(err)
+    loading.set_loading(bufnr, false)
+    vim.bo[bufnr].modifiable = true
+    vim.cmd.doautocmd({ args = { "BufReadPre", bufname }, mods = { silent = true } })
+    if err then
+      vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, vim.split(err, "\n"))
+    else
+      vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {})
+      vim.api.nvim_buf_call(bufnr, function()
+        vim.cmd.read({ args = { tmpfile }, mods = { silent = true } })
+      end)
+      vim.loop.fs_unlink(tmpfile)
+      vim.api.nvim_buf_set_lines(bufnr, 0, 1, true, {})
+    end
+    vim.bo[bufnr].modified = false
+    local filetype = vim.filetype.match({ buf = bufnr, filename = basename })
+    if filetype then
+      vim.bo[bufnr].filetype = filetype
+    end
+    vim.cmd.doautocmd({ args = { "BufReadPost", bufname }, mods = { silent = true } })
+    vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
+  end)
+end
+
+---@param bufnr integer
+M.write_file = function(bufnr)
+  local bufname = vim.api.nvim_buf_get_name(bufnr)
+  local url = M.parse_url(bufname)
+  local cache_dir = vim.fn.stdpath("cache")
+  assert(type(cache_dir) == "string")
+  local tmpdir = fs.join(cache_dir, "oil")
+  local fd, tmpfile = vim.loop.fs_mkstemp(fs.join(tmpdir, "s3_XXXXXXXX"))
+  if fd then
+    vim.loop.fs_close(fd)
+  end
+  vim.cmd.doautocmd({ args = { "BufWritePre", bufname }, mods = { silent = true } })
+  vim.bo[bufnr].modifiable = false
+  vim.cmd.write({ args = { tmpfile }, bang = true, mods = { silent = true, noautocmd = true } })
+  local tmp_bufnr = vim.fn.bufadd(tmpfile)
+
+  s3fs.cp(tmpfile, url_to_s3(url, false), false, function(err)
+    vim.bo[bufnr].modifiable = true
+    if err then
+      vim.notify(string.format("Error writing file: %s", err), vim.log.levels.ERROR)
+    else
+      vim.bo[bufnr].modified = false
+      vim.cmd.doautocmd({ args = { "BufWritePost", bufname }, mods = { silent = true } })
+    end
+    vim.loop.fs_unlink(tmpfile)
+    vim.api.nvim_buf_delete(tmp_bufnr, { force = true })
+  end)
+end
+
+return M
diff --git a/lua/oil/adapters/s3/s3fs.lua b/lua/oil/adapters/s3/s3fs.lua
new file mode 100644
index 0000000..2e16307
--- /dev/null
+++ b/lua/oil/adapters/s3/s3fs.lua
@@ -0,0 +1,149 @@
+local cache = require("oil.cache")
+local config = require("oil.config")
+local constants = require("oil.constants")
+local shell = require("oil.shell")
+local util = require("oil.util")
+
+local M = {}
+
+local FIELD_META = constants.FIELD_META
+
+---@param line string
+---@return string Name of entry
+---@return oil.EntryType
+---@return table Metadata for entry
+local function parse_ls_line_bucket(line)
+  local date, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(.*)$")
+  if not date or not name then
+    error(string.format("Could not parse '%s'", line))
+  end
+  local type = "directory"
+  local meta = { date = date }
+  return name, type, meta
+end
+
+---@param line string
+---@return string Name of entry
+---@return oil.EntryType
+---@return table Metadata for entry
+local function parse_ls_line_file(line)
+  local name = line:match("^%s+PRE%s+(.*)/$")
+  local type = "directory"
+  local meta = {}
+  if name then
+    return name, type, meta
+  end
+  local date, size
+  date, size, name = line:match("^(%d+%-%d+%-%d+%s%d+:%d+:%d+)%s+(%d+)%s+(.*)$")
+  if not name then
+    error(string.format("Could not parse '%s'", line))
+  end
+  type = "file"
+  meta = { date = date, size = tonumber(size) }
+  return name, type, meta
+end
+
+---@param cmd string[] cmd and flags
+---@return string[] Shell command to run
+local function create_s3_command(cmd)
+  local full_cmd = vim.list_extend({ "aws", "s3" }, cmd)
+  return vim.list_extend(full_cmd, config.extra_s3_args)
+end
+
+---@param url string
+---@param path string
+---@param callback fun(err?: string, entries?: oil.InternalEntry[], fetch_more?: fun())
+function M.list_dir(url, path, callback)
+  local cmd = create_s3_command({ "ls", path, "--color=off", "--no-cli-pager" })
+  shell.run(cmd, function(err, lines)
+    if err then
+      return callback(err)
+    end
+    assert(lines)
+    local cache_entries = {}
+    local url_path, _
+    _, url_path = util.parse_url(url)
+    local is_top_level = url_path == nil or url_path:match("/") == nil
+    local parse_ls_line = is_top_level and parse_ls_line_bucket or parse_ls_line_file
+    for _, line in ipairs(lines) do
+      if line ~= "" then
+        local name, type, meta = parse_ls_line(line)
+        -- in s3 '-' can be used to create an "empty folder"
+        if name ~= "-" then
+          local cache_entry = cache.create_entry(url, name, type)
+          table.insert(cache_entries, cache_entry)
+          cache_entry[FIELD_META] = meta
+        end
+      end
+    end
+    callback(nil, cache_entries)
+  end)
+end
+
+--- Create files
+---@param path string
+---@param callback fun(err: nil|string)
+function M.touch(path, callback)
+  -- here "-" means that we copy from stdin
+  local cmd = create_s3_command({ "cp", "-", path })
+  shell.run(cmd, { stdin = "null" }, callback)
+end
+
+--- Remove files
+---@param path string
+---@param is_folder boolean
+---@param callback fun(err: nil|string)
+function M.rm(path, is_folder, callback)
+  local main_cmd = { "rm", path }
+  if is_folder then
+    table.insert(main_cmd, "--recursive")
+  end
+  local cmd = create_s3_command(main_cmd)
+  shell.run(cmd, callback)
+end
+
+--- Remove bucket
+---@param bucket string
+---@param callback fun(err: nil|string)
+function M.rb(bucket, callback)
+  local cmd = create_s3_command({ "rb", bucket })
+  shell.run(cmd, callback)
+end
+
+--- Make bucket
+---@param bucket string
+---@param callback fun(err: nil|string)
+function M.mb(bucket, callback)
+  local cmd = create_s3_command({ "mb", bucket })
+  shell.run(cmd, callback)
+end
+
+--- Move files
+---@param src string
+---@param dest string
+---@param is_folder boolean
+---@param callback fun(err: nil|string)
+function M.mv(src, dest, is_folder, callback)
+  local main_cmd = { "mv", src, dest }
+  if is_folder then
+    table.insert(main_cmd, "--recursive")
+  end
+  local cmd = create_s3_command(main_cmd)
+  shell.run(cmd, callback)
+end
+
+--- Copy files
+---@param src string
+---@param dest string
+---@param is_folder boolean
+---@param callback fun(err: nil|string)
+function M.cp(src, dest, is_folder, callback)
+  local main_cmd = { "cp", src, dest }
+  if is_folder then
+    table.insert(main_cmd, "--recursive")
+  end
+  local cmd = create_s3_command(main_cmd)
+  shell.run(cmd, callback)
+end
+
+return M
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index 7b4e97c..d1b1d31 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -107,6 +107,8 @@ local default_config = {
   },
   -- Extra arguments to pass to SCP when moving/copying files over SSH
   extra_scp_args = {},
+  -- Extra arguments to pass to aws s3 when creating/deleting/moving/copying files using aws s3
+  extra_s3_args = {},
   -- EXPERIMENTAL support for performing file operations with git
   git = {
     -- Return true to automatically git add/mv/rm files
@@ -204,9 +206,14 @@ local default_config = {
 -- The adapter API hasn't really stabilized yet. We're not ready to advertise or encourage people to
 -- write their own adapters, and so there's no real reason to edit these config options. For that
 -- reason, I'm taking them out of the section above so they won't show up in the autogen docs.
+
+-- not "oil-s3://" on older neovim versions, since it doesn't open buffers correctly with a number
+-- in the name
+local oil_s3_string = vim.fn.has("nvim-0.12") == 1 and "oil-s3://" or "oil-sss://"
 default_config.adapters = {
   ["oil://"] = "files",
   ["oil-ssh://"] = "ssh",
+  [oil_s3_string] = "s3",
   ["oil-trash://"] = "trash",
 }
 default_config.adapter_aliases = {}
@@ -234,6 +241,7 @@ default_config.view_options.highlight_filename = nil
 ---@field use_default_keymaps boolean
 ---@field view_options oil.ViewOptions
 ---@field extra_scp_args string[]
+---@field extra_s3_args string[]
 ---@field git oil.GitOptions
 ---@field float oil.FloatWindowConfig
 ---@field preview_win oil.PreviewWindowConfig
@@ -262,6 +270,7 @@ local M = {}
 ---@field use_default_keymaps? boolean Set to false to disable all of the above keymaps
 ---@field view_options? oil.SetupViewOptions Configure which files are shown and how they are shown.
 ---@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH
+---@field extra_s3_args? string[] Extra arguments to pass to aws s3 when moving/copying files using aws s3
 ---@field git? oil.SetupGitOptions EXPERIMENTAL support for performing file operations with git
 ---@field float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float
 ---@field preview_win? oil.SetupPreviewWindowConfig Configuration for the file preview window
diff --git a/syntax/oil_preview.vim b/syntax/oil_preview.vim
index b6c2fab..2f14df9 100644
--- a/syntax/oil_preview.vim
+++ b/syntax/oil_preview.vim
@@ -2,9 +2,9 @@ if exists("b:current_syntax")
   finish
 endif
 
-syn match oilCreate /^CREATE /
+syn match oilCreate /^CREATE\( BUCKET\)\? /
 syn match oilMove   /^  MOVE /
-syn match oilDelete /^DELETE /
+syn match oilDelete /^DELETE\( BUCKET\)\? /
 syn match oilCopy   /^  COPY /
 syn match oilChange /^CHANGE /
 " Trash operations

From e5a1398790de2aa9eb7f795f592b8ee91a03eccc Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Sun, 30 Nov 2025 20:41:54 +0000
Subject: [PATCH 188/206] [docgen] Update docs skip-checks: true

---
 doc/oil.txt | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index fdc7ed1..300cca1 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -430,7 +430,7 @@ icon                                                                 *column-ico
                      the icon
 
 size                                                                 *column-size*
-    Adapters: files, ssh, s3
+    Adapters: files, ssh
     Sortable: this column can be used in view_props.sort
     The size of the file
 
@@ -478,7 +478,7 @@ atime                                                               *column-atim
       {format}    `string` Format string (see :help strftime)
 
 birthtime                                                       *column-birthtime*
-    Adapters: files, s3
+    Adapters: files
     Sortable: this column can be used in view_props.sort
     The time the file was created
 

From b9ab05fe5abad224c6d70b80a4b3f38e5201204b Mon Sep 17 00:00:00 2001
From: Siggsy 
Date: Sun, 30 Nov 2025 21:42:00 +0000
Subject: [PATCH 189/206] feat: add OilEmpty highlight group (#689)

* Add OilEmpty highlight

* Add OilEmpty to doc
---
 doc/oil.txt         | 3 +++
 lua/oil/columns.lua | 2 +-
 lua/oil/init.lua    | 5 +++++
 3 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index 300cca1..38a0d1a 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -646,6 +646,9 @@ yank_entry                                                    *actions.yank_entr
 --------------------------------------------------------------------------------
 HIGHLIGHTS                                                        *oil-highlights*
 
+OilEmpty                                                             *hl-OilEmpty*
+    Empty column values
+
 OilHidden                                                           *hl-OilHidden*
     Hidden entry in an oil buffer
 
diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua
index e3a4395..46ef96b 100644
--- a/lua/oil/columns.lua
+++ b/lua/oil/columns.lua
@@ -53,7 +53,7 @@ M.get_supported_columns = function(adapter_or_scheme)
   return ret
 end
 
-local EMPTY = { "-", "Comment" }
+local EMPTY = { "-", "OilEmpty" }
 
 M.EMPTY = EMPTY
 
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 44945bb..d730492 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -823,6 +823,11 @@ end
 ---@private
 M._get_highlights = function()
   return {
+    {
+      name = "OilEmpty",
+      link = "Comment",
+      desc = "Empty column values",
+    },
     {
       name = "OilHidden",
       link = "Comment",

From cbcb3f997f6f261c577b943ec94e4ef55108dd95 Mon Sep 17 00:00:00 2001
From: phanium <91544758+phanen@users.noreply.github.com>
Date: Mon, 1 Dec 2025 06:01:40 +0800
Subject: [PATCH 190/206] fix: command modifiers for :Oil (#691)

* Support command mods `:belowright hor 10Oil`
* Fix `:tab Oil` only work on the first tab
---
 lua/oil/init.lua | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index d730492..1abd5ba 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -1128,9 +1128,9 @@ M.setup = function(opts)
 
   config.setup(opts)
   set_colors()
-  vim.api.nvim_create_user_command("Oil", function(args)
+  local callback = function(args)
     local util = require("oil.util")
-    if args.smods.tab == 1 then
+    if args.smods.tab > 0 then
       vim.cmd.tabnew()
     end
     local float = false
@@ -1163,11 +1163,11 @@ M.setup = function(opts)
       end
     end
 
-    if not float and (args.smods.vertical or args.smods.split ~= "") then
+    if not float and (args.smods.vertical or args.smods.horizontal or args.smods.split ~= "") then
       if args.smods.vertical then
-        vim.cmd.vsplit({ mods = { split = args.smods.split } })
+        vim.cmd.vsplit({ mods = { split = args.smods.split }, range = { args.count } })
       else
-        vim.cmd.split({ mods = { split = args.smods.split } })
+        vim.cmd.split({ mods = { split = args.smods.split }, range = { args.count } })
       end
     end
 
@@ -1183,7 +1183,12 @@ M.setup = function(opts)
       open_opts.preview = {}
     end
     M[method](path, open_opts)
-  end, { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir" })
+  end
+  vim.api.nvim_create_user_command(
+    "Oil",
+    callback,
+    { desc = "Open oil file browser on a directory", nargs = "*", complete = "dir", count = true }
+  )
   local aug = vim.api.nvim_create_augroup("Oil", {})
 
   if config.default_file_explorer then

From 15a2b21eda3c94816b9449590425cdcffd8b836a Mon Sep 17 00:00:00 2001
From: Sebastian Lyng Johansen 
Date: Sat, 20 Dec 2025 21:03:07 +0100
Subject: [PATCH 191/206] fix: use `g~` instead of overriding the builtin `~`
 mapping (#694)

---
 lua/oil/config.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index d1b1d31..b4fca9a 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -69,7 +69,7 @@ local default_config = {
     ["-"] = { "actions.parent", mode = "n" },
     ["_"] = { "actions.open_cwd", mode = "n" },
     ["`"] = { "actions.cd", mode = "n" },
-    ["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
+    ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
     ["gs"] = { "actions.change_sort", mode = "n" },
     ["gx"] = "actions.open_external",
     ["g."] = { "actions.toggle_hidden", mode = "n" },

From 3b249b71957ee35d5adbfb21f77992809a4c4683 Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Sat, 20 Dec 2025 20:03:26 +0000
Subject: [PATCH 192/206] [docgen] Update docs skip-checks: true

---
 README.md   | 2 +-
 doc/oil.txt | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index deecf74..321bdba 100644
--- a/README.md
+++ b/README.md
@@ -204,7 +204,7 @@ require("oil").setup({
     ["-"] = { "actions.parent", mode = "n" },
     ["_"] = { "actions.open_cwd", mode = "n" },
     ["`"] = { "actions.cd", mode = "n" },
-    ["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
+    ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
     ["gs"] = { "actions.change_sort", mode = "n" },
     ["gx"] = "actions.open_external",
     ["g."] = { "actions.toggle_hidden", mode = "n" },
diff --git a/doc/oil.txt b/doc/oil.txt
index 38a0d1a..f9d56ae 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -86,7 +86,7 @@ CONFIG                                                                *oil-confi
         ["-"] = { "actions.parent", mode = "n" },
         ["_"] = { "actions.open_cwd", mode = "n" },
         ["`"] = { "actions.cd", mode = "n" },
-        ["~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
+        ["g~"] = { "actions.cd", opts = { scope = "tab" }, mode = "n" },
         ["gs"] = { "actions.change_sort", mode = "n" },
         ["gx"] = "actions.open_external",
         ["g."] = { "actions.toggle_hidden", mode = "n" },

From 09a4e4f4604bc73e557b336704637d0cf458459b Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Sun, 21 Dec 2025 15:14:45 -0500
Subject: [PATCH 193/206] ci: fix type error

---
 lua/oil/mutator/parser.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lua/oil/mutator/parser.lua b/lua/oil/mutator/parser.lua
index f25cbc9..bb22274 100644
--- a/lua/oil/mutator/parser.lua
+++ b/lua/oil/mutator/parser.lua
@@ -95,7 +95,7 @@ M.parse_line = function(adapter, line, column_defs)
     local name = util.split_config(def)
     local range = { start }
     local start_len = string.len(rem)
-    value, rem = columns.parse_col(adapter, rem, def)
+    value, rem = columns.parse_col(adapter, assert(rem), def)
     if not rem then
       return nil, string.format("Parsing %s failed", name)
     end

From 756dec855b4811f2d27f067a3aca477f368d99f5 Mon Sep 17 00:00:00 2001
From: jake-stewart <83528263+jake-stewart@users.noreply.github.com>
Date: Mon, 22 Dec 2025 04:26:10 +0800
Subject: [PATCH 194/206] feat: support multicursor.nvim (#696)

* support multicursor.nvim

* lint: apply stylua

---------

Co-authored-by: Steven Arcangeli 
---
 lua/oil/view.lua | 63 ++++++++++++++++++++++++++++++++++--------------
 1 file changed, 45 insertions(+), 18 deletions(-)

diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index e4145ad..999ae5c 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -257,24 +257,13 @@ local function get_first_mutable_column_col(adapter, ranges)
   return min_col
 end
 
----Force cursor to be after hidden/immutable columns
----@param bufnr integer
----@param mode false|"name"|"editable"
-local function constrain_cursor(bufnr, mode)
-  if not mode then
-    return
-  end
-  if bufnr ~= vim.api.nvim_get_current_buf() then
-    return
-  end
+--- @param bufnr integer
+--- @param adapter oil.Adapter
+--- @param mode false|"name"|"editable"
+--- @param cur integer[]
+--- @return integer[] | nil
+local function calc_constrained_cursor_pos(bufnr, adapter, mode, cur)
   local parser = require("oil.mutator.parser")
-
-  local adapter = util.get_adapter(bufnr, true)
-  if not adapter then
-    return
-  end
-
-  local cur = vim.api.nvim_win_get_cursor(0)
   local line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1]
   local column_defs = columns.get_supported_columns(adapter)
   local result = parser.parse_line(adapter, line, column_defs)
@@ -288,7 +277,45 @@ local function constrain_cursor(bufnr, mode)
       error(string.format('Unexpected value "%s" for option constrain_cursor', mode))
     end
     if cur[2] < min_col then
-      vim.api.nvim_win_set_cursor(0, { cur[1], min_col })
+      return { cur[1], min_col }
+    end
+  end
+end
+
+---Force cursor to be after hidden/immutable columns
+---@param bufnr integer
+---@param mode false|"name"|"editable"
+local function constrain_cursor(bufnr, mode)
+  if not mode then
+    return
+  end
+  if bufnr ~= vim.api.nvim_get_current_buf() then
+    return
+  end
+
+  local adapter = util.get_adapter(bufnr, true)
+  if not adapter then
+    return
+  end
+
+  local mc = package.loaded["multicursor-nvim"]
+  if mc then
+    mc.onSafeState(function()
+      mc.action(function(ctx)
+        ctx:forEachCursor(function(cursor)
+          local new_cur =
+            calc_constrained_cursor_pos(bufnr, adapter, mode, { cursor:line(), cursor:col() - 1 })
+          if new_cur then
+            cursor:setPos({ new_cur[1], new_cur[2] + 1 })
+          end
+        end)
+      end)
+    end, { once = true })
+  else
+    local cur = vim.api.nvim_win_get_cursor(0)
+    local new_cur = calc_constrained_cursor_pos(bufnr, adapter, mode, cur)
+    if new_cur then
+      vim.api.nvim_win_set_cursor(0, new_cur)
     end
   end
 end

From bbfa7cba85967dc9dbfe0ca486fd4b17084bf846 Mon Sep 17 00:00:00 2001
From: phanium <91544758+phanen@users.noreply.github.com>
Date: Sun, 28 Dec 2025 05:27:37 +0800
Subject: [PATCH 195/206] fix: args.count of 0 is not used as size (#695)

---
 lua/oil/init.lua | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 1abd5ba..80462db 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -1164,10 +1164,12 @@ M.setup = function(opts)
     end
 
     if not float and (args.smods.vertical or args.smods.horizontal or args.smods.split ~= "") then
+      local range = args.count > 0 and { args.count } or nil
+      local cmdargs = { mods = { split = args.smods.split }, range = range }
       if args.smods.vertical then
-        vim.cmd.vsplit({ mods = { split = args.smods.split }, range = { args.count } })
+        vim.cmd.vsplit(cmdargs)
       else
-        vim.cmd.split({ mods = { split = args.smods.split }, range = { args.count } })
+        vim.cmd.split(cmdargs)
       end
     end
 

From 634049414b9ba7b827f050dc5a873bf68944c640 Mon Sep 17 00:00:00 2001
From: Muhammad Imaduddin 
Date: Tue, 30 Dec 2025 01:15:58 +0700
Subject: [PATCH 196/206] fix: open files under cwd with relative name (#693)

---
 lua/oil/adapters/files.lua | 2 +-
 tests/files_spec.lua       | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 28e647b..9aa54c3 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -277,7 +277,7 @@ M.normalize_url = function(url, callback)
           local norm_path = util.addslash(fs.os_to_posix_path(realpath))
           callback(scheme .. norm_path)
         else
-          callback(realpath)
+          callback(vim.fn.fnamemodify(realpath, ":."))
         end
       end)
     )
diff --git a/tests/files_spec.lua b/tests/files_spec.lua
index 66a70d0..4333d80 100644
--- a/tests/files_spec.lua
+++ b/tests/files_spec.lua
@@ -168,5 +168,6 @@ a.describe("files adapter", function()
     test_util.wait_for_autocmd("BufReadPost")
     assert.equals("ruby", vim.bo.filetype)
     assert.equals(vim.fn.fnamemodify(tmpdir.path, ":p") .. "file.rb", vim.api.nvim_buf_get_name(0))
+    assert.equals(tmpdir.path .. "/file.rb", vim.fn.bufname())
   end)
 end)

From 963c8d2c5538c4a896a31f396b8a5a2683e254dc Mon Sep 17 00:00:00 2001
From: Dominic Della Valle 
Date: Mon, 29 Dec 2025 15:27:20 -0500
Subject: [PATCH 197/206] fix: handle empty LSP glob patterns (#702)

* fix: handle empty LSP glob patterns

* fix: use non-greedy pattern matching

* lint: fix shadowed variable

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 lua/oil/lsp/workspace.lua | 27 ++++++++++++++++++---------
 1 file changed, 18 insertions(+), 9 deletions(-)

diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua
index d113b13..8e48276 100644
--- a/lua/oil/lsp/workspace.lua
+++ b/lua/oil/lsp/workspace.lua
@@ -76,17 +76,26 @@ local function get_matching_paths(client, filters, paths)
       ---@type string|vim.lpeg.Pattern
       local glob_to_match = glob
       if vim.glob and vim.glob.to_lpeg then
-        -- HACK around https://github.com/neovim/neovim/issues/28931
-        -- find alternations and sort them by length to try to match the longest first
-        if vim.fn.has("nvim-0.11") == 0 then
-          glob = glob:gsub("{(.*)}", function(s)
-            local pieces = vim.split(s, ",")
-            table.sort(pieces, function(a, b)
+        glob = glob:gsub("{(.-)}", function(s)
+          local patterns = vim.split(s, ",")
+          local filtered = {}
+          for _, pat in ipairs(patterns) do
+            if pat ~= "" then
+              table.insert(filtered, pat)
+            end
+          end
+          if #filtered == 0 then
+            return ""
+          end
+          -- HACK around https://github.com/neovim/neovim/issues/28931
+          -- find alternations and sort them by length to try to match the longest first
+          if vim.fn.has("nvim-0.11") == 0 then
+            table.sort(filtered, function(a, b)
               return a:len() > b:len()
             end)
-            return "{" .. table.concat(pieces, ",") .. "}"
-          end)
-        end
+          end
+          return "{" .. table.concat(filtered, ",") .. "}"
+        end)
 
         glob_to_match = vim.glob.to_lpeg(glob)
       end

From 78ed0cf7d9a64280d621960af4be7872aa650417 Mon Sep 17 00:00:00 2001
From: jake-stewart <83528263+jake-stewart@users.noreply.github.com>
Date: Thu, 1 Jan 2026 04:50:39 +0800
Subject: [PATCH 198/206] fix: multicursor when opened with --preview (#701)

---
 lua/oil/init.lua | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 80462db..0c56720 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -543,6 +543,8 @@ M.open_preview = function(opts, callback)
   end
 
   util.get_edit_path(bufnr, entry, function(normalized_url)
+    local mc = package.loaded["multicursor-nvim"]
+    local has_multicursors = mc and mc.hasCursors()
     local is_visual_mode = util.is_visual_mode()
     if preview_win then
       if is_visual_mode then
@@ -601,7 +603,10 @@ M.open_preview = function(opts, callback)
     end
     vim.w.oil_entry_id = entry.id
     vim.w.oil_source_win = prev_win
-    if is_visual_mode then
+    if has_multicursors then
+      hack_set_win(prev_win)
+      mc.restoreCursors()
+    elseif is_visual_mode then
       hack_set_win(prev_win)
       -- Restore the visual selection
       vim.cmd.normal({ args = { "gv" }, bang = true })

From 81b8a91735ad5cd24a6b3137f14a89f19176364f Mon Sep 17 00:00:00 2001
From: Steven Arcangeli 
Date: Thu, 1 Jan 2026 00:22:21 -0500
Subject: [PATCH 199/206] cleanup: remove deprecated trash_command

---
 .github/workflows/tests.yml      |  1 +
 lua/oil/adapters/files.lua       | 11 +-------
 lua/oil/adapters/files/trash.lua | 44 --------------------------------
 lua/oil/config.lua               |  8 ------
 4 files changed, 2 insertions(+), 62 deletions(-)
 delete mode 100644 lua/oil/adapters/files/trash.lua

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 6d0945c..2b62853 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -4,6 +4,7 @@ on:
   push:
     branches:
       - master
+      - stevearc-*
   pull_request:
     branches:
       - master
diff --git a/lua/oil/adapters/files.lua b/lua/oil/adapters/files.lua
index 9aa54c3..9785c3a 100644
--- a/lua/oil/adapters/files.lua
+++ b/lua/oil/adapters/files.lua
@@ -6,7 +6,6 @@ local fs = require("oil.fs")
 local git = require("oil.git")
 local log = require("oil.log")
 local permissions = require("oil.adapters.files.permissions")
-local trash = require("oil.adapters.files.trash")
 local util = require("oil.util")
 local uv = vim.uv or vim.loop
 
@@ -620,15 +619,7 @@ M.perform_action = function(action, cb)
     end
 
     if config.delete_to_trash then
-      if config.trash_command then
-        vim.notify_once(
-          "Oil now has native support for trash. Remove the `trash_command` from your config to try it out!",
-          vim.log.levels.WARN
-        )
-        trash.recursive_delete(path, cb)
-      else
-        require("oil.adapters.trash").delete_to_trash(path, cb)
-      end
+      require("oil.adapters.trash").delete_to_trash(path, cb)
     else
       fs.recursive_delete(action.entry_type, path, cb)
     end
diff --git a/lua/oil/adapters/files/trash.lua b/lua/oil/adapters/files/trash.lua
deleted file mode 100644
index 5236f5b..0000000
--- a/lua/oil/adapters/files/trash.lua
+++ /dev/null
@@ -1,44 +0,0 @@
-local config = require("oil.config")
-local M = {}
-
-M.recursive_delete = function(path, cb)
-  local stdout = {}
-  local stderr = {}
-  local cmd
-  if config.trash_command:find("%s") then
-    cmd = string.format("%s %s", config.trash_command, vim.fn.shellescape(path))
-  else
-    cmd = { config.trash_command, path }
-  end
-  local jid = vim.fn.jobstart(cmd, {
-    stdout_buffered = true,
-    stderr_buffered = true,
-    on_stdout = function(j, output)
-      stdout = output
-    end,
-    on_stderr = function(j, output)
-      stderr = output
-    end,
-    on_exit = function(j, exit_code)
-      if exit_code == 0 then
-        cb()
-      else
-        cb(
-          string.format(
-            "Error moving '%s' to trash:\n  stdout: %s\n  stderr: %s",
-            path,
-            table.concat(stdout, "\n  "),
-            table.concat(stderr, "\n  ")
-          )
-        )
-      end
-    end,
-  })
-  if jid == 0 then
-    cb(string.format("Passed invalid argument '%s' to '%s'", path, config.trash_command))
-  elseif jid == -1 then
-    cb(string.format("'%s' is not executable", config.trash_command))
-  end
-end
-
-return M
diff --git a/lua/oil/config.lua b/lua/oil/config.lua
index b4fca9a..cafa783 100644
--- a/lua/oil/config.lua
+++ b/lua/oil/config.lua
@@ -224,7 +224,6 @@ default_config.view_options.highlight_filename = nil
 ---@class oil.Config
 ---@field adapters table Hidden from SetupOpts
 ---@field adapter_aliases table Hidden from SetupOpts
----@field trash_command? string Deprecated option that we should clean up soon
 ---@field silence_scp_warning? boolean Undocumented option
 ---@field default_file_explorer boolean
 ---@field columns oil.ColumnSpec[]
@@ -403,13 +402,6 @@ local M = {}
 M.setup = function(opts)
   opts = opts or {}
 
-  if opts.trash_command then
-    vim.notify(
-      "[oil.nvim] trash_command is deprecated. Use built-in trash functionality instead (:help oil-trash).\nCompatibility will be removed on 2025-06-01.",
-      vim.log.levels.WARN
-    )
-  end
-
   local new_conf = vim.tbl_deep_extend("keep", opts, default_config)
   if not new_conf.use_default_keymaps then
     new_conf.keymaps = opts.keymaps or {}

From 24055701b7e712e6071086201b7e43837a6fb54c Mon Sep 17 00:00:00 2001
From: Ross <38072549+jay-rdc@users.noreply.github.com>
Date: Mon, 12 Jan 2026 05:53:17 +0800
Subject: [PATCH 200/206] feat: add horizontal scrolling actions (#709)

* feat: add horizontal scrolling actions

* refactor(actions): remove unnecessary use of `nvim_replace_termcodes`

* lint: apply stylua

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 doc/oil.txt         |  6 ++++++
 lua/oil/actions.lua | 24 ++++++++++++++++++++++++
 2 files changed, 30 insertions(+)

diff --git a/doc/oil.txt b/doc/oil.txt
index f9d56ae..ab3f462 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -596,6 +596,12 @@ preview_scroll_down                                  *actions.preview_scroll_dow
 preview_scroll_up                                      *actions.preview_scroll_up*
     Scroll up in the preview window
 
+preview_scroll_left                                  *actions.preview_scroll_left*
+    Scroll left in the preview window
+
+preview_scroll_right                                *actions.preview_scroll_right*
+    Scroll right in the preview window
+
 refresh                                                          *actions.refresh*
     Refresh current directory list
 
diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua
index ead4f51..1c61999 100644
--- a/lua/oil/actions.lua
+++ b/lua/oil/actions.lua
@@ -136,6 +136,30 @@ M.preview_scroll_up = {
   end,
 }
 
+M.preview_scroll_left = {
+  desc = "Scroll left in the preview window",
+  callback = function()
+    local winid = util.get_preview_win()
+    if winid then
+      vim.api.nvim_win_call(winid, function()
+        vim.cmd.normal({ "zH", bang = true })
+      end)
+    end
+  end,
+}
+
+M.preview_scroll_right = {
+  desc = "Scroll right in the preview window",
+  callback = function()
+    local winid = util.get_preview_win()
+    if winid then
+      vim.api.nvim_win_call(winid, function()
+        vim.cmd.normal({ "zL", bang = true })
+      end)
+    end
+  end,
+}
+
 M.parent = {
   desc = "Navigate to the parent path",
   callback = oil.open,

From 43227c5a1cc70b30294da7b657c866726166740f Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Sun, 11 Jan 2026 21:53:37 +0000
Subject: [PATCH 201/206] [docgen] Update docs skip-checks: true

---
 doc/oil.txt | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index ab3f462..a909740 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -593,15 +593,15 @@ preview                                                          *actions.previe
 preview_scroll_down                                  *actions.preview_scroll_down*
     Scroll down in the preview window
 
-preview_scroll_up                                      *actions.preview_scroll_up*
-    Scroll up in the preview window
-
 preview_scroll_left                                  *actions.preview_scroll_left*
     Scroll left in the preview window
 
 preview_scroll_right                                *actions.preview_scroll_right*
     Scroll right in the preview window
 
+preview_scroll_up                                      *actions.preview_scroll_up*
+    Scroll up in the preview window
+
 refresh                                                          *actions.refresh*
     Refresh current directory list
 

From d278dc40f9de9980868a0a55fa666fba5e6aeacb Mon Sep 17 00:00:00 2001
From: Sebastian Oberhoff 
Date: Sun, 11 Jan 2026 22:55:32 +0100
Subject: [PATCH 202/206] fix: propagate errors in recursive_delete and
 recursive_copy (#712)

The `complete` callback checks `err` instead of `err2`, but `err` is
always nil inside the `elseif entries` branch. This silently ignores
child operation errors, causing misleading "directory not empty" failures.
---
 lua/oil/fs.lua | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lua/oil/fs.lua b/lua/oil/fs.lua
index a9ae10c..f169c0b 100644
--- a/lua/oil/fs.lua
+++ b/lua/oil/fs.lua
@@ -218,7 +218,7 @@ M.recursive_delete = function(entry_type, path, cb)
           local waiting = #entries
           local complete
           complete = function(err2)
-            if err then
+            if err2 then
               complete = function() end
               return inner_cb(err2)
             end
@@ -320,7 +320,7 @@ M.recursive_copy = function(entry_type, src_path, dest_path, cb)
               local waiting = #entries
               local complete
               complete = function(err2)
-                if err then
+                if err2 then
                   complete = function() end
                   return inner_cb(err2)
                 end

From fbbb2a98721da86f46b06994fd3b4833b04c6e9b Mon Sep 17 00:00:00 2001
From: Daniel Kongsgaard 
Date: Mon, 12 Jan 2026 20:26:43 +0100
Subject: [PATCH 203/206] doc: fix s3 column descriptions (#715)

---
 doc/oil.txt         | 4 ++--
 scripts/generate.py | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index a909740..33ae00a 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -430,7 +430,7 @@ icon                                                                 *column-ico
                      the icon
 
 size                                                                 *column-size*
-    Adapters: files, ssh
+    Adapters: files, ssh, s3
     Sortable: this column can be used in view_props.sort
     The size of the file
 
@@ -478,7 +478,7 @@ atime                                                               *column-atim
       {format}    `string` Format string (see :help strftime)
 
 birthtime                                                       *column-birthtime*
-    Adapters: files
+    Adapters: files, s3
     Sortable: this column can be used in view_props.sort
     The time the file was created
 
diff --git a/scripts/generate.py b/scripts/generate.py
index fac256d..6e347fd 100755
--- a/scripts/generate.py
+++ b/scripts/generate.py
@@ -151,7 +151,7 @@ COL_DEFS = [
             ),
         ],
     ),
-    ColumnDef("size", "files, ssh", False, True, "The size of the file", HL + []),
+    ColumnDef("size", "files, ssh, s3", False, True, "The size of the file", HL + []),
     ColumnDef(
         "permissions",
         "files, ssh",
@@ -171,7 +171,7 @@ COL_DEFS = [
     ),
     ColumnDef(
         "birthtime",
-        "files",
+        "files, s3",
         False,
         True,
         "The time the file was created",

From 6b59a6cf623fa2245c7454ddb458df5bdb6615d3 Mon Sep 17 00:00:00 2001
From: malewicz1337 <132208688+malewicz1337@users.noreply.github.com>
Date: Wed, 14 Jan 2026 06:28:16 +0100
Subject: [PATCH 204/206] feat: add support for column text alignment (#711)

* feat: add support for column text alignment

* refactor(util): replace rpad with pad_align

* refactor(columns): whitespace handling in parse_col

* refactor: small changes

* doc: add align option to doc generation

* refactor: replace lpad with pad_align

---------

Co-authored-by: Steven Arcangeli 
---
 doc/oil.txt             |  8 ++++++
 lua/oil/columns.lua     |  4 +--
 lua/oil/keymap_util.lua |  2 +-
 lua/oil/loading.lua     |  3 +-
 lua/oil/util.lua        | 63 +++++++++++++++++++++--------------------
 lua/oil/view.lua        |  7 +++--
 scripts/generate.py     | 23 +++++++++------
 7 files changed, 64 insertions(+), 46 deletions(-)

diff --git a/doc/oil.txt b/doc/oil.txt
index 33ae00a..7738156 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -414,6 +414,7 @@ type                                                                 *column-typ
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
+      {align}     `"left"|"center"|"right"` Text alignment within the column
       {icons}     `table` Mapping of entry type to icon
 
 icon                                                                 *column-icon*
@@ -423,6 +424,7 @@ icon                                                                 *column-ico
     Parameters:
       {highlight}    `string|fun(value: string): string` Highlight group, or
                      function that returns a highlight group
+      {align}        `"left"|"center"|"right"` Text alignment within the column
       {default_file} `string` Fallback icon for files when nvim-web-devicons
                      returns nil
       {directory}    `string` Icon for directories
@@ -437,6 +439,7 @@ size                                                                 *column-siz
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
+      {align}     `"left"|"center"|"right"` Text alignment within the column
 
 permissions                                                   *column-permissions*
     Adapters: files, ssh
@@ -446,6 +449,7 @@ permissions                                                   *column-permission
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
+      {align}     `"left"|"center"|"right"` Text alignment within the column
 
 ctime                                                               *column-ctime*
     Adapters: files
@@ -455,6 +459,7 @@ ctime                                                               *column-ctim
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
+      {align}     `"left"|"center"|"right"` Text alignment within the column
       {format}    `string` Format string (see :help strftime)
 
 mtime                                                               *column-mtime*
@@ -465,6 +470,7 @@ mtime                                                               *column-mtim
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
+      {align}     `"left"|"center"|"right"` Text alignment within the column
       {format}    `string` Format string (see :help strftime)
 
 atime                                                               *column-atime*
@@ -475,6 +481,7 @@ atime                                                               *column-atim
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
+      {align}     `"left"|"center"|"right"` Text alignment within the column
       {format}    `string` Format string (see :help strftime)
 
 birthtime                                                       *column-birthtime*
@@ -485,6 +492,7 @@ birthtime                                                       *column-birthtim
     Parameters:
       {highlight} `string|fun(value: string): string` Highlight group, or
                   function that returns a highlight group
+      {align}     `"left"|"center"|"right"` Text alignment within the column
       {format}    `string` Format string (see :help strftime)
 
 --------------------------------------------------------------------------------
diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua
index 46ef96b..975576f 100644
--- a/lua/oil/columns.lua
+++ b/lua/oil/columns.lua
@@ -98,13 +98,13 @@ end
 M.parse_col = function(adapter, line, col_def)
   local name, conf = util.split_config(col_def)
   -- If rendering failed, there will just be a "-"
-  local empty_col, rem = line:match("^(-%s+)(.*)$")
+  local empty_col, rem = line:match("^%s*(-%s+)(.*)$")
   if empty_col then
     return nil, rem
   end
   local column = M.get_column(adapter, name)
   if column then
-    return column.parse(line, conf)
+    return column.parse(line:gsub("^%s+", ""), conf)
   end
 end
 
diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua
index 04b8066..8c58738 100644
--- a/lua/oil/keymap_util.lua
+++ b/lua/oil/keymap_util.lua
@@ -108,7 +108,7 @@ M.show_help = function(keymaps)
   local highlights = {}
   local max_line = 1
   for _, entry in ipairs(keymap_entries) do
-    local line = string.format(" %s   %s", util.rpad(entry.str, max_lhs), entry.desc)
+    local line = string.format(" %s   %s", util.pad_align(entry.str, max_lhs, "left"), entry.desc)
     max_line = math.max(max_line, vim.api.nvim_strwidth(line))
     table.insert(lines, line)
     local start = 1
diff --git a/lua/oil/loading.lua b/lua/oil/loading.lua
index 35a6bba..6e575c5 100644
--- a/lua/oil/loading.lua
+++ b/lua/oil/loading.lua
@@ -74,7 +74,8 @@ M.set_loading = function(bufnr, is_loading)
             M.set_loading(bufnr, false)
             return
           end
-          local lines = { util.lpad("Loading", math.floor(width / 2) - 3), bar_iter() }
+          local lines =
+            { util.pad_align("Loading", math.floor(width / 2) - 3, "right"), bar_iter() }
           util.render_text(bufnr, lines)
         end)
       )
diff --git a/lua/oil/util.lua b/lua/oil/util.lua
index 894027e..0ec1acd 100644
--- a/lua/oil/util.lua
+++ b/lua/oil/util.lua
@@ -91,34 +91,28 @@ M.get_adapter = function(bufnr, silent)
 end
 
 ---@param text string
----@param length nil|integer
----@return string
-M.rpad = function(text, length)
-  if not length then
-    return text
+---@param width integer|nil
+---@param align oil.ColumnAlign
+---@return string padded_text
+---@return integer left_padding
+M.pad_align = function(text, width, align)
+  if not width then
+    return text, 0
   end
-  local textlen = vim.api.nvim_strwidth(text)
-  local delta = length - textlen
-  if delta > 0 then
-    return text .. string.rep(" ", delta)
-  else
-    return text
+  local text_width = vim.api.nvim_strwidth(text)
+  local total_pad = width - text_width
+  if total_pad <= 0 then
+    return text, 0
   end
-end
 
----@param text string
----@param length nil|integer
----@return string
-M.lpad = function(text, length)
-  if not length then
-    return text
-  end
-  local textlen = vim.api.nvim_strwidth(text)
-  local delta = length - textlen
-  if delta > 0 then
-    return string.rep(" ", delta) .. text
+  if align == "right" then
+    return string.rep(" ", total_pad) .. text, total_pad
+  elseif align == "center" then
+    local left_pad = math.floor(total_pad / 2)
+    local right_pad = total_pad - left_pad
+    return string.rep(" ", left_pad) .. text .. string.rep(" ", right_pad), left_pad
   else
-    return text
+    return text .. string.rep(" ", total_pad), 0
   end
 end
 
@@ -314,11 +308,15 @@ M.split_config = function(name_or_config)
   end
 end
 
+---@alias oil.ColumnAlign "left"|"center"|"right"
+
 ---@param lines oil.TextChunk[][]
 ---@param col_width integer[]
+---@param col_align? oil.ColumnAlign[]
 ---@return string[]
 ---@return any[][] List of highlights {group, lnum, col_start, col_end}
-M.render_table = function(lines, col_width)
+M.render_table = function(lines, col_width, col_align)
+  col_align = col_align or {}
   local str_lines = {}
   local highlights = {}
   for _, cols in ipairs(lines) do
@@ -332,9 +330,12 @@ M.render_table = function(lines, col_width)
       else
         text = chunk
       end
-      text = M.rpad(text, col_width[i])
+
+      local unpadded_len = text:len()
+      local padding
+      text, padding = M.pad_align(text, col_width[i], col_align[i] or "left")
+
       table.insert(pieces, text)
-      local col_end = col + text:len() + 1
       if hl then
         if type(hl) == "table" then
           -- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[]
@@ -344,15 +345,15 @@ M.render_table = function(lines, col_width)
             table.insert(highlights, {
               sub_hl[1],
               #str_lines,
-              col + sub_hl[2],
-              col + sub_hl[3],
+              col + padding + sub_hl[2],
+              col + padding + sub_hl[3],
             })
           end
         else
-          table.insert(highlights, { hl, #str_lines, col, col_end })
+          table.insert(highlights, { hl, #str_lines, col + padding, col + padding + unpadded_len })
         end
       end
-      col = col_end
+      col = col + text:len() + 1
     end
     table.insert(str_lines, table.concat(pieces, " "))
   end
diff --git a/lua/oil/view.lua b/lua/oil/view.lua
index 999ae5c..b3a216e 100644
--- a/lua/oil/view.lua
+++ b/lua/oil/view.lua
@@ -668,8 +668,11 @@ local function render_buffer(bufnr, opts)
   local column_defs = columns.get_supported_columns(scheme)
   local line_table = {}
   local col_width = {}
-  for i in ipairs(column_defs) do
+  local col_align = {}
+  for i, col_def in ipairs(column_defs) do
     col_width[i + 1] = 1
+    local _, conf = util.split_config(col_def)
+    col_align[i + 1] = conf and conf.align or "left"
   end
 
   if M.should_display("..", bufnr) then
@@ -692,7 +695,7 @@ local function render_buffer(bufnr, opts)
     end
   end
 
-  local lines, highlights = util.render_table(line_table, col_width)
+  local lines, highlights = util.render_table(line_table, col_width, col_align)
 
   vim.bo[bufnr].modifiable = true
   vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
diff --git a/scripts/generate.py b/scripts/generate.py
index 6e347fd..4e02550 100755
--- a/scripts/generate.py
+++ b/scripts/generate.py
@@ -110,11 +110,16 @@ class ColumnDef:
     params: List["LuaParam"] = field(default_factory=list)
 
 
-HL = [
+UNIVERSAL = [
     LuaParam(
         "highlight",
         "string|fun(value: string): string",
         "Highlight group, or function that returns a highlight group",
+    ),
+    LuaParam(
+        "align",
+        '"left"|"center"|"right"',
+        "Text alignment within the column",
     )
 ]
 TIME = [
@@ -127,7 +132,7 @@ COL_DEFS = [
         False,
         True,
         "The type of the entry (file, directory, link, etc)",
-        HL
+        UNIVERSAL
         + [LuaParam("icons", "table", "Mapping of entry type to icon")],
     ),
     ColumnDef(
@@ -136,7 +141,7 @@ COL_DEFS = [
         False,
         False,
         "An icon for the entry's type (requires nvim-web-devicons)",
-        HL
+        UNIVERSAL
         + [
             LuaParam(
                 "default_file",
@@ -151,23 +156,23 @@ COL_DEFS = [
             ),
         ],
     ),
-    ColumnDef("size", "files, ssh, s3", False, True, "The size of the file", HL + []),
+    ColumnDef("size", "files, ssh, s3", False, True, "The size of the file", UNIVERSAL + []),
     ColumnDef(
         "permissions",
         "files, ssh",
         True,
         False,
         "Access permissions of the file",
-        HL + [],
+        UNIVERSAL + [],
     ),
     ColumnDef(
-        "ctime", "files", False, True, "Change timestamp of the file", HL + TIME + []
+        "ctime", "files", False, True, "Change timestamp of the file", UNIVERSAL + TIME + []
     ),
     ColumnDef(
-        "mtime", "files", False, True, "Last modified time of the file", HL + TIME + []
+        "mtime", "files", False, True, "Last modified time of the file", UNIVERSAL + TIME + []
     ),
     ColumnDef(
-        "atime", "files", False, True, "Last access time of the file", HL + TIME + []
+        "atime", "files", False, True, "Last access time of the file", UNIVERSAL + TIME + []
     ),
     ColumnDef(
         "birthtime",
@@ -175,7 +180,7 @@ COL_DEFS = [
         False,
         True,
         "The time the file was created",
-        HL + TIME + [],
+        UNIVERSAL + TIME + [],
     ),
 ]
 

From 7a09f0b000e8dae148e4267927d9aad4e3926434 Mon Sep 17 00:00:00 2001
From: zeta-squared <84503911+zeta-squared@users.noreply.github.com>
Date: Sat, 17 Jan 2026 16:01:02 +1100
Subject: [PATCH 205/206] fix: add `open_float` params to `toggle_float` (#716)

* feat: `toggle_float` now takes the same params as `open_float`

* docs: update `toggle_float` docs for `opts` and `cb` params

* fix: ensure cb is always called

---------

Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
---
 README.md        |  2 +-
 doc/api.md       | 21 ++++++++++++++-------
 lua/oil/init.lua |  9 +++++++--
 3 files changed, 22 insertions(+), 10 deletions(-)

diff --git a/README.md b/README.md
index 321bdba..8e0dba0 100644
--- a/README.md
+++ b/README.md
@@ -394,7 +394,7 @@ These are plugins maintained by other authors that extend the functionality of o
 - [toggle_hidden()](doc/api.md#toggle_hidden)
 - [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr)
 - [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb)
-- [toggle_float(dir)](doc/api.md#toggle_floatdir)
+- [toggle_float(dir, opts, cb)](doc/api.md#toggle_floatdir)
 - [open(dir, opts, cb)](doc/api.md#opendir-opts-cb)
 - [close(opts)](doc/api.md#closeopts)
 - [open_preview(opts, callback)](doc/api.md#open_previewopts-callback)
diff --git a/doc/api.md b/doc/api.md
index f4577c4..7a10791 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -11,7 +11,7 @@
 - [toggle_hidden()](#toggle_hidden)
 - [get_current_dir(bufnr)](#get_current_dirbufnr)
 - [open_float(dir, opts, cb)](#open_floatdir-opts-cb)
-- [toggle_float(dir)](#toggle_floatdir)
+- [toggle_float(dir, opts, cb)](#toggle_floatdir)
 - [open(dir, opts, cb)](#opendir-opts-cb)
 - [close(opts)](#closeopts)
 - [open_preview(opts, callback)](#open_previewopts-callback)
@@ -107,14 +107,21 @@ Open oil browser in a floating window
 | >>split      | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier                                                                              |
 | cb           | `nil\|fun()`                                            | Called after the oil buffer is ready                                                        |
 
-## toggle_float(dir)
+## toggle_float(dir, opts, cb)
 
-`toggle_float(dir)` \
-Open oil browser in a floating window, or close it if open
+`toggle_float(dir, opts, cb)` \
+Open oil browser in a floating window, or close it if open. Parameters only apply when the floating window is
+opened
 
-| Param | Type          | Desc                                                                                        |
-| ----- | ------------- | ------------------------------------------------------------------------------------------- |
-| dir   | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
+| Param        | Type                                                    | Desc                                                                                        |
+| ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| dir          | `nil\|string`                                           | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file |
+| opts         | `nil\|oil.OpenOpts`                                     |                                                                                             |
+| >preview     | `nil\|oil.OpenPreviewOpts`                              | When present, open the preview window after opening oil                                     |
+| >>vertical   | `nil\|boolean`                                          | Open the buffer in a vertical split                                                         |
+| >>horizontal | `nil\|boolean`                                          | Open the buffer in a horizontal split                                                       |
+| >>split      | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier                                                                              |
+| cb           | `nil\|fun()`                                            | Called after the oil buffer is ready                                                        |
 
 ## open(dir, opts, cb)
 
diff --git a/lua/oil/init.lua b/lua/oil/init.lua
index 0c56720..908d6dd 100644
--- a/lua/oil/init.lua
+++ b/lua/oil/init.lua
@@ -342,11 +342,16 @@ end
 
 ---Open oil browser in a floating window, or close it if open
 ---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
-M.toggle_float = function(dir)
+---@param opts? oil.OpenOpts
+---@param cb? fun() Called after the oil buffer is ready
+M.toggle_float = function(dir, opts, cb)
   if vim.w.is_oil_win then
     M.close()
+    if cb then
+      cb()
+    end
   else
-    M.open_float(dir)
+    M.open_float(dir, opts, cb)
   end
 end
 

From f55b25e493a7df76371cfadd0ded5004cb9cd48a Mon Sep 17 00:00:00 2001
From: Github Actions 
Date: Sat, 17 Jan 2026 05:01:19 +0000
Subject: [PATCH 206/206] [docgen] Update docs skip-checks: true

---
 README.md   |  2 +-
 doc/api.md  |  5 ++---
 doc/oil.txt | 14 +++++++++++---
 3 files changed, 14 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 8e0dba0..bf12bed 100644
--- a/README.md
+++ b/README.md
@@ -394,7 +394,7 @@ These are plugins maintained by other authors that extend the functionality of o
 - [toggle_hidden()](doc/api.md#toggle_hidden)
 - [get_current_dir(bufnr)](doc/api.md#get_current_dirbufnr)
 - [open_float(dir, opts, cb)](doc/api.md#open_floatdir-opts-cb)
-- [toggle_float(dir, opts, cb)](doc/api.md#toggle_floatdir)
+- [toggle_float(dir, opts, cb)](doc/api.md#toggle_floatdir-opts-cb)
 - [open(dir, opts, cb)](doc/api.md#opendir-opts-cb)
 - [close(opts)](doc/api.md#closeopts)
 - [open_preview(opts, callback)](doc/api.md#open_previewopts-callback)
diff --git a/doc/api.md b/doc/api.md
index 7a10791..24d4a50 100644
--- a/doc/api.md
+++ b/doc/api.md
@@ -11,7 +11,7 @@
 - [toggle_hidden()](#toggle_hidden)
 - [get_current_dir(bufnr)](#get_current_dirbufnr)
 - [open_float(dir, opts, cb)](#open_floatdir-opts-cb)
-- [toggle_float(dir, opts, cb)](#toggle_floatdir)
+- [toggle_float(dir, opts, cb)](#toggle_floatdir-opts-cb)
 - [open(dir, opts, cb)](#opendir-opts-cb)
 - [close(opts)](#closeopts)
 - [open_preview(opts, callback)](#open_previewopts-callback)
@@ -110,8 +110,7 @@ Open oil browser in a floating window
 ## toggle_float(dir, opts, cb)
 
 `toggle_float(dir, opts, cb)` \
-Open oil browser in a floating window, or close it if open. Parameters only apply when the floating window is
-opened
+Open oil browser in a floating window, or close it if open
 
 | Param        | Type                                                    | Desc                                                                                        |
 | ------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
diff --git a/doc/oil.txt b/doc/oil.txt
index 7738156..8753f87 100644
--- a/doc/oil.txt
+++ b/doc/oil.txt
@@ -321,12 +321,20 @@ open_float({dir}, {opts}, {cb})                                   *oil.open_floa
                            plit modifier
       {cb}   `nil|fun()` Called after the oil buffer is ready
 
-toggle_float({dir})                                             *oil.toggle_float*
+toggle_float({dir}, {opts}, {cb})                               *oil.toggle_float*
     Open oil browser in a floating window, or close it if open
 
     Parameters:
-      {dir} `nil|string` When nil, open the parent of the current buffer, or the
-            cwd if current buffer is not a file
+      {dir}  `nil|string` When nil, open the parent of the current buffer, or
+             the cwd if current buffer is not a file
+      {opts} `nil|oil.OpenOpts`
+          {preview} `nil|oil.OpenPreviewOpts` When present, open the preview
+                    window after opening oil
+              {vertical}   `nil|boolean` Open the buffer in a vertical split
+              {horizontal} `nil|boolean` Open the buffer in a horizontal split
+              {split}      `nil|"aboveleft"|"belowright"|"topleft"|"botright"` S
+                           plit modifier
+      {cb}   `nil|fun()` Called after the oil buffer is ready
 
 open({dir}, {opts}, {cb})                                               *oil.open*
     Open oil browser for a directory