canola.nvim/lua/oil/mutator/confirmation.lua
Barrett Ruth 86f553cd0a
build: replace luacheck with selene, add nix devshell and pre-commit (#20)
* build: replace luacheck with selene

Problem: luacheck is unmaintained (last release 2018) and required
suppressing four warning classes to avoid false positives. It also
lacks first-class vim/neovim awareness.

Solution: switch to selene with std='vim' for vim-aware linting.
Replace the luacheck CI job with selene, update the Makefile lint
target, and delete .luacheckrc.

* build: add nix devshell and pre-commit hooks

Problem: oil.nvim had no reproducible dev environment. The .envrc
set up a Python venv for the now-removed docgen pipeline, and there
were no pre-commit hooks for local formatting checks.

Solution: add flake.nix with stylua, selene, and prettier in the
devshell. Replace the stale Python .envrc with 'use flake'. Add
.pre-commit-config.yaml with stylua and prettier hooks matching
other plugins in the repo collection.

* fix: format with stylua

* build(selene): configure lints and add inline suppressions

Problem: selene fails on 5 errors and 3 warnings from upstream code
patterns that are intentional (mixed tables in config API, unused
callback parameters, identical if branches for readability).

Solution: globally allow mixed_table and unused_variable (high volume,
inherent to the codebase design). Add inline selene:allow directives
for the 8 remaining issues: if_same_then_else (4), mismatched_arg_count
(1), empty_if (2), global_usage (1). Remove .envrc from tracking.

* build: switch typecheck action to mrcjkb/lua-typecheck-action

Problem: oil.nvim used stevearc/nvim-typecheck-action, which required
cloning the action repo locally for the Makefile lint target. All
other plugins in the collection use mrcjkb/lua-typecheck-action.

Solution: swap to mrcjkb/lua-typecheck-action@v0 for consistency.
Remove the nvim-typecheck-action git clone from the Makefile and
.gitignore. Drop LuaLS from the local lint target since it requires
a full language server install — CI handles it.
2026-02-21 23:52:27 -05:00

192 lines
5.6 KiB
Lua

local columns = require('oil.columns')
local config = require('oil.config')
local layout = require('oil.layout')
local util = require('oil.util')
local M = {}
---@param actions oil.Action[]
---@return boolean
local function is_simple_edit(actions)
local num_create = 0
local num_copy = 0
local num_move = 0
for _, action in ipairs(actions) do
-- If there are any deletes, it is not a simple edit
if action.type == 'delete' then
return false
elseif action.type == 'create' then
num_create = num_create + 1
elseif action.type == 'copy' then
num_copy = num_copy + 1
-- Cross-adapter copies are not simple
if util.parse_url(action.src_url) ~= util.parse_url(action.dest_url) then
return false
end
elseif action.type == 'move' then
num_move = num_move + 1
-- Cross-adapter moves are not simple
if util.parse_url(action.src_url) ~= util.parse_url(action.dest_url) then
return false
end
end
end
-- More than one move/copy is complex
if num_move > 1 or num_copy > 1 then
return false
end
-- More than 5 creates is complex
if num_create > 5 then
return false
end
return true
end
---@param winid integer
---@param bufnr integer
---@param lines string[]
local function render_lines(winid, bufnr, lines)
util.render_text(bufnr, lines, {
v_align = 'top',
h_align = 'left',
winid = winid,
actions = { '[Y]es', '[N]o' },
})
end
---@param actions oil.Action[]
---@param should_confirm nil|boolean
---@param cb fun(proceed: boolean)
M.show = vim.schedule_wrap(function(actions, should_confirm, cb)
-- The schedule wrap ensures that we actually enter the floating window.
-- Not sure why it doesn't work without that
if should_confirm == false or #actions == 0 then
cb(true)
return
end
if should_confirm == nil and config.skip_confirm_for_simple_edits and is_simple_edit(actions) then
cb(true)
return
end
-- Create the buffer
local bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].bufhidden = 'wipe'
local lines = {}
local max_line_width = 0
for _, action in ipairs(actions) do
local adapter = util.get_adapter_for_action(action)
local line
if action.type == 'change' then
---@cast action oil.ChangeAction
line = columns.render_change_action(adapter, action)
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
max_line_width = line_width
end
end
table.insert(lines, '')
-- Create the floating window
local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation)
local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, {
relative = 'editor',
width = width,
height = height,
row = math.floor((layout.get_editor_height() - height) / 2),
col = math.floor((layout.get_editor_width() - width) / 2),
zindex = 152, -- render on top of the floating window title
style = 'minimal',
border = config.confirmation.border,
})
if not ok then
vim.notify(string.format('Error showing oil preview window: %s', winid), vim.log.levels.ERROR)
cb(false)
end
vim.bo[bufnr].filetype = 'oil_preview'
vim.bo[bufnr].syntax = 'oil_preview'
for k, v in pairs(config.confirmation.win_options) do
vim.api.nvim_set_option_value(k, v, { scope = 'local', win = winid })
end
render_lines(winid, bufnr, lines)
local restore_cursor = util.hide_cursor()
-- Attach autocmds and keymaps
local cancel
local confirm
local autocmds = {}
local function make_callback(value)
return function()
confirm = function() end
cancel = function() end
for _, id in ipairs(autocmds) do
vim.api.nvim_del_autocmd(id)
end
autocmds = {}
vim.api.nvim_win_close(winid, true)
restore_cursor()
cb(value)
end
end
cancel = make_callback(false)
confirm = make_callback(true)
vim.api.nvim_create_autocmd('BufLeave', {
callback = function()
cancel()
end,
once = true,
nested = true,
buffer = bufnr,
})
vim.api.nvim_create_autocmd('WinLeave', {
callback = function()
cancel()
end,
once = true,
nested = true,
})
table.insert(
autocmds,
vim.api.nvim_create_autocmd('VimResized', {
callback = function()
if vim.api.nvim_win_is_valid(winid) then
width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation)
vim.api.nvim_win_set_config(winid, {
relative = 'editor',
width = width,
height = height,
row = math.floor((layout.get_editor_height() - height) / 2),
col = math.floor((layout.get_editor_width() - width) / 2),
zindex = 152, -- render on top of the floating window title
})
render_lines(winid, bufnr, lines)
end
end,
})
)
-- We used to use [C]ancel to cancel, so preserve the old keymap
local cancel_keys = { 'n', 'N', 'c', 'C', 'q', '<C-c>', '<Esc>' }
for _, cancel_key in ipairs(cancel_keys) do
vim.keymap.set('n', cancel_key, function()
cancel()
end, { buffer = bufnr, nowait = true })
end
-- We used to use [O]k to confirm, so preserve the old keymap
local confirm_keys = { 'y', 'Y', 'o', 'O' }
for _, confirm_key in ipairs(confirm_keys) do
vim.keymap.set('n', confirm_key, function()
confirm()
end, { buffer = bufnr, nowait = true })
end
end)
return M