fix: support visual mode when preview window is open (#315)

This commit is contained in:
Steven Arcangeli 2024-04-19 15:26:34 -04:00
parent fa3820ebf1
commit f41d7e7cd8
8 changed files with 219 additions and 103 deletions

View file

@ -56,7 +56,7 @@ M.preview = {
return
end
end
oil.select({ preview = true })
oil.open_preview()
end,
}

View file

@ -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

View file

@ -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

View file

@ -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