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 001/109] 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 002/109] 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 003/109] [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 004/109] 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 005/109] 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 006/109] 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 007/109] 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 008/109] 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 009/109] 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 010/109] 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 011/109] 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 012/109] 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 013/109] 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 014/109] [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 015/109] 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 016/109] 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 017/109] 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 018/109] 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 019/109] 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 020/109] 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 021/109] 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 022/109] 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 023/109] 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 024/109] [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 025/109] 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 026/109] 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 027/109] 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 028/109] 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 029/109] 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 030/109] 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 031/109] 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 032/109] 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 033/109] 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 034/109] 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 035/109] 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 036/109] 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 037/109] [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 038/109] 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 039/109] 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 040/109] 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 041/109] 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 042/109] 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 043/109] 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 044/109] 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 045/109] 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 046/109] 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 047/109] 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 048/109] 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 049/109] 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 050/109] 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 051/109] 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 052/109] 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 053/109] 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 054/109] 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 055/109] 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 056/109] 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 057/109] [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 058/109] 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 059/109] 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 060/109] 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 061/109] 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 062/109] 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 063/109] 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 064/109] 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 065/109] 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 066/109] 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 067/109] 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 068/109] 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 069/109] 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 070/109] 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 071/109] 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 072/109] 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 073/109] 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 074/109] 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 075/109] 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 076/109] 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 077/109] 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 078/109] 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 079/109] 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 080/109] 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 081/109] 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 082/109] 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 083/109] 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 084/109] 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 085/109] [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 086/109] 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 087/109] 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 088/109] 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 089/109] 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 090/109] 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 091/109] [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 092/109] 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 093/109] 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 094/109] 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 095/109] [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 096/109] 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 097/109] 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 098/109] 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 099/109] 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 100/109] 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 101/109] 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 102/109] 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 103/109] 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 104/109] [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 105/109] 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 106/109] 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 107/109] 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 108/109] 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 109/109] [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