diff --git a/README.md b/README.md index 972409f..bbf4437 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i - [toggle_float(dir)](doc/api.md#toggle_floatdir) - [open(dir)](doc/api.md#opendir) - [close()](doc/api.md#close) +- [open_preview(opts)](doc/api.md#open_previewopts) - [select(opts, callback)](doc/api.md#selectopts-callback) - [save(opts, cb)](doc/api.md#saveopts-cb) - [setup(opts)](doc/api.md#setupopts) diff --git a/doc/api.md b/doc/api.md index 38d042d..7221036 100644 --- a/doc/api.md +++ b/doc/api.md @@ -14,6 +14,7 @@ - [toggle_float(dir)](#toggle_floatdir) - [open(dir)](#opendir) - [close()](#close) +- [open_preview(opts)](#open_previewopts) - [select(opts, callback)](#selectopts-callback) - [save(opts, cb)](#saveopts-cb) - [setup(opts)](#setupopts) @@ -116,6 +117,18 @@ Open oil browser for a directory Restore the buffer that was present when oil was opened +## open_preview(opts) + +`open_preview(opts)` \ +Preview the entry under the cursor in a split + +| Param | Type | Desc | | +| ----- | ------------ | -------------------------------------------------- | ------------------------------------- | +| opts | `nil\|table` | | | +| | vertical | `boolean` | Open the buffer in a vertical split | +| | horizontal | `boolean` | Open the buffer in a horizontal split | +| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | + ## select(opts, callback) `select(opts, callback)` \ @@ -127,7 +140,6 @@ Select the entry under the cursor | | vertical | `boolean` | Open the buffer in a vertical split | | | horizontal | `boolean` | Open the buffer in a horizontal split | | | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| | preview | `boolean` | Open the buffer in a preview window | | | tab | `boolean` | Open the buffer in a new tab | | | close | `boolean` | Close the original oil buffer once selection is made | | callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | | diff --git a/doc/oil.txt b/doc/oil.txt index 176a0cb..68a7531 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -282,6 +282,16 @@ close() *oil.clos Restore the buffer that was present when oil was opened +open_preview({opts}) *oil.open_preview* + Preview the entry under the cursor in a split + + Parameters: + {opts} `nil|table` + {vertical} `boolean` Open the buffer in a vertical split + {horizontal} `boolean` Open the buffer in a horizontal split + {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + modifier + select({opts}, {callback}) *oil.select* Select the entry under the cursor @@ -291,7 +301,6 @@ select({opts}, {callback}) *oil.selec {horizontal} `boolean` Open the buffer in a horizontal split {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split modifier - {preview} `boolean` Open the buffer in a preview window {tab} `boolean` Open the buffer in a new tab {close} `boolean` Close the original oil buffer once selection is made diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 1e3cdb3..cebd6b1 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -56,7 +56,7 @@ M.preview = { return end end - oil.select({ preview = true }) + oil.open_preview() end, } diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 207ff50..4fd4b68 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -5,6 +5,7 @@ local M = {} ---@field type oil.EntryType ---@field id nil|integer Will be nil if it hasn't been persisted to disk yet ---@field parsed_name nil|string +---@field meta nil|table ---@alias oil.EntryType "file"|"directory"|"socket"|"link"|"fifo" ---@alias oil.HlRange { [1]: string, [2]: integer, [3]: integer } A tuple of highlight group name, col_start, col_end @@ -372,13 +373,13 @@ local function update_preview_window(oil_bufnr) local util = require("oil.util") util.run_after_load(oil_bufnr, function() local cursor_entry = M.get_cursor_entry() - if cursor_entry then - local preview_win_id = util.get_preview_win() - if preview_win_id then - if cursor_entry.id ~= vim.w[preview_win_id].oil_entry_id then - M.select({ preview = true }) - end - end + local preview_win_id = util.get_preview_win() + if + cursor_entry + and preview_win_id + and cursor_entry.id ~= vim.w[preview_win_id].oil_entry_id + then + M.open_preview() end end) end @@ -437,22 +438,15 @@ M.close = function() vim.api.nvim_buf_delete(oilbuf, { force = true }) end ----Select the entry under the cursor +---Preview the entry under the cursor in a split ---@param opts nil|table --- vertical boolean Open the buffer in a vertical split --- horizontal boolean Open the buffer in a horizontal split --- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier ---- preview boolean Open the buffer in a preview window ---- tab boolean Open the buffer in a new tab ---- close boolean Close the original oil buffer once selection is made ----@param callback nil|fun(err: nil|string) Called once all entries have been opened -M.select = function(opts, callback) - local cache = require("oil.cache") - local config = require("oil.config") - local constants = require("oil.constants") - local pathutil = require("oil.pathutil") - local FIELD_META = constants.FIELD_META - opts = vim.tbl_extend("keep", opts or {}, {}) +M.open_preview = function(opts, callback) + opts = opts or {} + local util = require("oil.util") + local function finish(err) if err then vim.notify(err, vim.log.levels.ERROR) @@ -461,29 +455,135 @@ M.select = function(opts, callback) callback(err) end end - if opts.preview and not opts.horizontal and opts.vertical == nil then + + if not opts.horizontal and opts.vertical == nil then opts.vertical = true end - if not opts.split and (opts.horizontal or opts.vertical or opts.preview) then + if not opts.split then if opts.horizontal then opts.split = vim.o.splitbelow and "belowright" or "aboveleft" else opts.split = vim.o.splitright and "belowright" or "aboveleft" end end - if opts.tab and (opts.preview or opts.split) then - return finish("Cannot set preview or split when tab = true") - end - if opts.close and opts.preview then - return finish("Cannot use close=true with preview=true") - end - local util = require("oil.util") - if util.is_floating_win() and opts.preview then + if util.is_floating_win() then return finish("oil preview doesn't work in a floating window") end + + local entry = M.get_cursor_entry() + if not entry then + return finish("Could not find entry under cursor") + end + + local preview_win = util.get_preview_win() + local prev_win = vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_get_current_buf() + + local cmd = preview_win and "buffer" or "sbuffer" + local mods = { + vertical = opts.vertical, + horizontal = opts.horizontal, + split = opts.split, + } + + local is_visual_mode = util.is_visual_mode() + -- HACK Switching windows takes us out of visual mode. + -- Switching with nvim_set_current_win causes the previous visual selection (as used by `gv`) to + -- not get set properly. So we have to switch windows this way instead. + local hack_set_win = function(winid) + local winnr = vim.api.nvim_win_get_number(winid) + vim.cmd.wincmd({ args = { "w" }, count = winnr }) + end + + if preview_win then + if is_visual_mode then + hack_set_win(preview_win) + else + vim.api.nvim_set_current_win(preview_win) + end + end + + util.get_edit_path(bufnr, entry, function(normalized_url) + local filebufnr = vim.fn.bufadd(normalized_url) + local entry_is_file = not vim.endswith(normalized_url, "/") + + -- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after + -- we close the window + if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then + vim.bo[filebufnr].bufhidden = "wipe" + vim.b[filebufnr].oil_preview_buffer = true + end + + ---@diagnostic disable-next-line: param-type-mismatch + local ok, err = pcall(vim.cmd, { + cmd = cmd, + args = { filebufnr }, + mods = mods, + }) + -- Ignore swapfile errors + if not ok and err and not err:match("^Vim:E325:") then + vim.api.nvim_echo({ { err, "Error" } }, true, {}) + end + + vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 }) + vim.w.oil_entry_id = entry.id + vim.w.oil_source_win = prev_win + if is_visual_mode then + hack_set_win(prev_win) + -- Restore the visual selection + vim.cmd.normal({ args = { "gv" }, bang = true }) + else + vim.api.nvim_set_current_win(prev_win) + end + finish() + end) +end + +---Select the entry under the cursor +---@param opts nil|table +--- vertical boolean Open the buffer in a vertical split +--- horizontal boolean Open the buffer in a horizontal split +--- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier +--- tab boolean Open the buffer in a new tab +--- close boolean Close the original oil buffer once selection is made +---@param callback nil|fun(err: nil|string) Called once all entries have been opened +M.select = function(opts, callback) + local cache = require("oil.cache") + local config = require("oil.config") + local constants = require("oil.constants") + local util = require("oil.util") + local FIELD_META = constants.FIELD_META + opts = vim.tbl_extend("keep", opts or {}, {}) + + if opts.preview then + vim.notify_once( + "Deprecated: do not call oil.select with preview=true. Use oil.open_preview instead.\nThis shim will be removed on 2025-01-01" + ) + M.open_preview(opts, callback) + return + end + + local function finish(err) + if err then + vim.notify(err, vim.log.levels.ERROR) + end + if callback then + callback(err) + end + end + if not opts.split and (opts.horizontal or opts.vertical) then + if opts.horizontal then + opts.split = vim.o.splitbelow and "belowright" or "aboveleft" + else + opts.split = vim.o.splitright and "belowright" or "aboveleft" + end + end + if opts.tab and opts.split then + return finish("Cannot use split=true when tab = true") + end local adapter = util.get_adapter(0) if not adapter then - return finish("Could not find adapter for current buffer") + return finish("Not an oil buffer") end local visual_range = util.get_visual_range() @@ -506,10 +606,6 @@ M.select = function(opts, callback) if vim.tbl_isempty(entries) then return finish("Could not find entry under cursor") end - if #entries > 1 and opts.preview then - vim.notify("Cannot preview multiple entries", vim.log.levels.WARN) - entries = { entries[1] } - end -- Check if any of these entries are moved from their original location local bufname = vim.api.nvim_buf_get_name(0) @@ -533,7 +629,7 @@ M.select = function(opts, callback) end end end - if any_moved and not opts.preview and config.prompt_save_on_select_new_entry then + if any_moved and config.prompt_save_on_select_new_entry then local ok, choice = pcall(vim.fn.confirm, "Save changes?", "Yes\nNo", 1) if not ok then return finish() @@ -543,11 +639,8 @@ M.select = function(opts, callback) end end - local preview_win = util.get_preview_win() local prev_win = vim.api.nvim_get_current_win() - local scheme, dir = util.parse_url(bufname) - assert(scheme and dir) -- Async iter over entries so we can normalize the url before opening local i = 1 local function open_next_entry(cb) @@ -556,16 +649,7 @@ M.select = function(opts, callback) if not entry then return cb() end - local url = scheme .. dir .. entry.name - local is_directory = entry.type == "directory" - or ( - entry.type == "link" - and entry.meta - and entry.meta.link_stat - and entry.meta.link_stat.type == "directory" - ) - if is_directory then - url = url .. "/" + if util.is_directory(entry) then -- If this is a new directory BUT we think we already have an entry with this name, disallow -- entry. This prevents the case of MOVE /foo -> /bar + CREATE /foo. -- If you enter the new /foo, it will show the contents of the old /foo. @@ -573,29 +657,15 @@ M.select = function(opts, callback) return cb("Please save changes before entering new directory") end else + -- Close floating window before opening a file if vim.w.is_oil_win then vim.api.nvim_win_close(0, false) end end - local get_edit_path - if entry.name == ".." then - get_edit_path = function(edit_cb) - edit_cb(scheme .. pathutil.parent(dir)) - end - elseif adapter.get_entry_path then - get_edit_path = function(edit_cb) - adapter.get_entry_path(url, entry, edit_cb) - end - else - get_edit_path = function(edit_cb) - adapter.normalize_url(url, edit_cb) - end - end - -- Normalize the url before opening to prevent needing to rename them inside the BufReadCmd -- Renaming buffers during opening can lead to missed autocmds - get_edit_path(function(normalized_url) + util.get_edit_path(0, entry, function(normalized_url) local mods = { vertical = opts.vertical, horizontal = opts.horizontal, @@ -605,32 +675,17 @@ M.select = function(opts, callback) local filebufnr = vim.fn.bufadd(normalized_url) local entry_is_file = not vim.endswith(normalized_url, "/") - if opts.preview then - -- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after - -- we close the window - if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then - vim.bo[filebufnr].bufhidden = "wipe" - vim.b[filebufnr].oil_preview_buffer = true - end - elseif entry_is_file then - -- The :buffer command doesn't set buflisted=true - -- So do that for non-diretory-buffers + -- The :buffer command doesn't set buflisted=true + -- So do that for normal files or for oil dirs if config set buflisted=true + if entry_is_file or config.buf_options.buflisted then vim.bo[filebufnr].buflisted = true end - local cmd - if opts.preview and preview_win then - vim.api.nvim_set_current_win(preview_win) - cmd = "buffer" - else - if opts.tab then - vim.cmd.tabnew({ mods = mods }) - cmd = "buffer" - elseif opts.split then - cmd = "sbuffer" - else - cmd = "buffer" - end + local cmd = "buffer" + if opts.tab then + vim.cmd.tabnew({ mods = mods }) + elseif opts.split then + cmd = "sbuffer" end ---@diagnostic disable-next-line: param-type-mismatch local ok, err = pcall(vim.cmd, { @@ -643,12 +698,6 @@ M.select = function(opts, callback) vim.api.nvim_echo({ { err, "Error" } }, true, {}) end - if opts.preview then - vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 }) - vim.w.oil_entry_id = entry.id - vim.w.oil_source_win = prev_win - vim.api.nvim_set_current_win(prev_win) - end open_next_entry(cb) end) end diff --git a/lua/oil/util.lua b/lua/oil/util.lua index b19c003..211dd78 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -756,12 +756,16 @@ M.send_to_quickfix = function(opts) vim.api.nvim_exec_autocmds("QuickFixCmdPost", {}) end +---@return boolean +M.is_visual_mode = function() + local mode = vim.api.nvim_get_mode().mode + return mode:match("^[vV]") ~= nil +end + ---Get the current visual selection range. If not in visual mode, return nil. ---@return {start_lnum: integer, end_lnum: integer}? M.get_visual_range = function() - local mode = vim.api.nvim_get_mode().mode - local is_visual = mode:match("^[vV]") - if not is_visual then + if not M.is_visual_mode() then return end -- This is the best way to get the visual selection at the moment @@ -796,4 +800,43 @@ M.run_after_load = function(bufnr, callback) end end +---@param entry oil.Entry +---@return boolean +M.is_directory = function(entry) + local is_directory = entry.type == "directory" + or ( + entry.type == "link" + and entry.meta + and entry.meta.link_stat + and entry.meta.link_stat.type == "directory" + ) + return is_directory == true +end + +---Get the :edit path for an entry +---@param bufnr integer The oil buffer that contains the entry +---@param entry oil.Entry +---@param callback fun(normalized_url: string) +M.get_edit_path = function(bufnr, entry, callback) + local pathutil = require("oil.pathutil") + + local bufname = vim.api.nvim_buf_get_name(bufnr) + local scheme, dir = M.parse_url(bufname) + local adapter = M.get_adapter(bufnr) + assert(scheme and dir and adapter) + + local url = scheme .. dir .. entry.name + if M.is_directory(entry) then + url = url .. "/" + end + + if entry.name == ".." then + callback(scheme .. pathutil.parent(dir)) + elseif adapter.get_entry_path then + adapter.get_entry_path(url, entry, callback) + else + adapter.normalize_url(url, callback) + end +end + return M diff --git a/lua/oil/view.lua b/lua/oil/view.lua index a5032ee..adea825 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -389,7 +389,7 @@ M.initialize = function(bufnr) vim.schedule(constrain_cursor) end, }) - vim.api.nvim_create_autocmd("CursorMoved", { + vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, { desc = "Update oil preview window", group = "Oil", buffer = bufnr, @@ -420,11 +420,13 @@ M.initialize = function(bufnr) return end local entry = oil.get_cursor_entry() - if entry then + -- Don't update in visual mode. Visual mode implies editing not browsing, + -- and updating the preview can cause flicker and stutter. + if entry and not util.is_visual_mode() then local winid = util.get_preview_win() if winid then if entry.id ~= vim.w[winid].oil_entry_id then - oil.select({ preview = true }) + oil.open_preview() end end end diff --git a/tests/altbuf_spec.lua b/tests/altbuf_spec.lua index 2f1e8e3..4b51604 100644 --- a/tests/altbuf_spec.lua +++ b/tests/altbuf_spec.lua @@ -104,7 +104,7 @@ a.describe("Alternate buffer", function() return oil.get_cursor_entry() end, 10) vim.api.nvim_win_set_cursor(0, { 1, 1 }) - oil.select({ preview = true }) + oil.open_preview() test_util.wait_for_autocmd({ "User", pattern = "OilEnter" }) assert.equals("foo", vim.fn.expand("#")) end)