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.
This commit is contained in:
Barrett Ruth 2026-02-21 23:52:27 -05:00 committed by GitHub
parent df53b172a9
commit 86f553cd0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 2762 additions and 2649 deletions

View file

@ -1,16 +1,16 @@
local Progress = require("oil.mutator.progress")
local Trie = require("oil.mutator.trie")
local cache = require("oil.cache")
local columns = require("oil.columns")
local config = require("oil.config")
local confirmation = require("oil.mutator.confirmation")
local constants = require("oil.constants")
local fs = require("oil.fs")
local lsp_helpers = require("oil.lsp.helpers")
local oil = require("oil")
local parser = require("oil.mutator.parser")
local util = require("oil.util")
local view = require("oil.view")
local Progress = require('oil.mutator.progress')
local Trie = require('oil.mutator.trie')
local cache = require('oil.cache')
local columns = require('oil.columns')
local config = require('oil.config')
local confirmation = require('oil.mutator.confirmation')
local constants = require('oil.constants')
local fs = require('oil.fs')
local lsp_helpers = require('oil.lsp.helpers')
local oil = require('oil')
local parser = require('oil.mutator.parser')
local util = require('oil.util')
local view = require('oil.view')
local M = {}
local FIELD_NAME = constants.FIELD_NAME
@ -73,7 +73,7 @@ M.create_actions_from_diffs = function(all_diffs)
local function add_action(action)
local adapter = assert(config.get_adapter_by_scheme(action.dest_url or action.url))
if not adapter.filter_action or adapter.filter_action(action) then
if action.type == "create" then
if action.type == 'create' then
if seen_creates[action.url] then
return
else
@ -87,11 +87,11 @@ M.create_actions_from_diffs = function(all_diffs)
for bufnr, diffs in pairs(all_diffs) do
local adapter = util.get_adapter(bufnr, true)
if not adapter then
error("Missing adapter")
error('Missing adapter')
end
local parent_url = vim.api.nvim_buf_get_name(bufnr)
for _, diff in ipairs(diffs) do
if diff.type == "new" then
if diff.type == 'new' then
if diff.id then
local by_id = diff_by_id[diff.id]
---HACK: set the destination on this diff for use later
@ -100,28 +100,28 @@ M.create_actions_from_diffs = function(all_diffs)
table.insert(by_id, diff)
else
-- Parse nested files like foo/bar/baz
local path_sep = fs.is_windows and "[/\\]" or "/"
local path_sep = fs.is_windows and '[/\\]' or '/'
local pieces = vim.split(diff.name, path_sep)
local url = parent_url:gsub("/$", "")
local url = parent_url:gsub('/$', '')
for i, v in ipairs(pieces) do
local is_last = i == #pieces
local entry_type = is_last and diff.entry_type or "directory"
local alternation = v:match("{([^}]+)}")
local entry_type = is_last and diff.entry_type or 'directory'
local alternation = v:match('{([^}]+)}')
if is_last and alternation then
-- Parse alternations like foo.{js,test.js}
for _, alt in ipairs(vim.split(alternation, ",")) do
local alt_url = url .. "/" .. v:gsub("{[^}]+}", alt)
for _, alt in ipairs(vim.split(alternation, ',')) do
local alt_url = url .. '/' .. v:gsub('{[^}]+}', alt)
add_action({
type = "create",
type = 'create',
url = alt_url,
entry_type = entry_type,
link = diff.link,
})
end
else
url = url .. "/" .. v
url = url .. '/' .. v
add_action({
type = "create",
type = 'create',
url = url,
entry_type = entry_type,
link = diff.link,
@ -129,9 +129,9 @@ M.create_actions_from_diffs = function(all_diffs)
end
end
end
elseif diff.type == "change" then
elseif diff.type == 'change' then
add_action({
type = "change",
type = 'change',
url = parent_url .. diff.name,
entry_type = diff.entry_type,
column = diff.column,
@ -151,7 +151,7 @@ M.create_actions_from_diffs = function(all_diffs)
for id, diffs in pairs(diff_by_id) do
local entry = cache.get_entry_by_id(id)
if not entry then
error(string.format("Could not find entry %d", id))
error(string.format('Could not find entry %d', id))
end
---HACK: access the has_delete field on the list-like table of diffs
---@diagnostic disable-next-line: undefined-field
@ -161,7 +161,7 @@ M.create_actions_from_diffs = function(all_diffs)
-- MOVE (+ optional copies) when has both creates and delete
for i, diff in ipairs(diffs) do
add_action({
type = i == #diffs and "move" or "copy",
type = i == #diffs and 'move' or 'copy',
entry_type = entry[FIELD_TYPE],
---HACK: access the dest field we set above
---@diagnostic disable-next-line: undefined-field
@ -172,7 +172,7 @@ M.create_actions_from_diffs = function(all_diffs)
else
-- DELETE when no create
add_action({
type = "delete",
type = 'delete',
entry_type = entry[FIELD_TYPE],
url = cache.get_parent_url(id) .. entry[FIELD_NAME],
})
@ -181,7 +181,7 @@ M.create_actions_from_diffs = function(all_diffs)
-- COPY when create but no delete
for _, diff in ipairs(diffs) do
add_action({
type = "copy",
type = 'copy',
entry_type = entry[FIELD_TYPE],
src_url = cache.get_parent_url(id) .. entry[FIELD_NAME],
---HACK: access the dest field we set above
@ -201,9 +201,9 @@ M.enforce_action_order = function(actions)
local src_trie = Trie.new()
local dest_trie = Trie.new()
for _, action in ipairs(actions) do
if action.type == "delete" or action.type == "change" then
if action.type == 'delete' or action.type == 'change' then
src_trie:insert_action(action.url, action)
elseif action.type == "create" then
elseif action.type == 'create' then
dest_trie:insert_action(action.url, action)
else
dest_trie:insert_action(action.dest_url, action)
@ -223,18 +223,18 @@ M.enforce_action_order = function(actions)
---@param action oil.Action
local function get_deps(action)
local ret = {}
if action.type == "delete" then
if action.type == 'delete' then
src_trie:accum_children_of(action.url, ret)
elseif action.type == "create" then
elseif action.type == 'create' then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE NEW /a/b
dest_trie:accum_first_parents_of(action.url, ret)
-- Process remove path before creating new path
-- e.g. DELETE /a BEFORE NEW /a
src_trie:accum_actions_at(action.url, ret, function(a)
return a.type == "move" or a.type == "delete"
return a.type == 'move' or a.type == 'delete'
end)
elseif action.type == "change" then
elseif action.type == 'change' then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE CHANGE /a/b
dest_trie:accum_first_parents_of(action.url, ret)
@ -244,9 +244,9 @@ M.enforce_action_order = function(actions)
-- Finish copy from operations first
-- e.g. COPY /a -> /b BEFORE CHANGE /a
src_trie:accum_actions_at(action.url, ret, function(entry)
return entry.type == "copy"
return entry.type == 'copy'
end)
elseif action.type == "move" then
elseif action.type == 'move' then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE MOVE /z -> /a/b
dest_trie:accum_first_parents_of(action.dest_url, ret)
@ -260,9 +260,9 @@ M.enforce_action_order = function(actions)
-- Process remove path before moving to new path
-- e.g. MOVE /a -> /b BEFORE MOVE /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a)
return a.type == "move" or a.type == "delete"
return a.type == 'move' or a.type == 'delete'
end)
elseif action.type == "copy" then
elseif action.type == 'copy' then
-- Finish operating on parents first
-- e.g. NEW /a BEFORE COPY /z -> /a/b
dest_trie:accum_first_parents_of(action.dest_url, ret)
@ -272,7 +272,7 @@ M.enforce_action_order = function(actions)
-- Process remove path before copying to new path
-- e.g. MOVE /a -> /b BEFORE COPY /c -> /a
src_trie:accum_actions_at(action.dest_url, ret, function(a)
return a.type == "move" or a.type == "delete"
return a.type == 'move' or a.type == 'delete'
end)
end
return ret
@ -312,24 +312,24 @@ M.enforce_action_order = function(actions)
if selected then
to_remove = selected
else
if loop_action and loop_action.type == "move" then
if loop_action and loop_action.type == 'move' then
-- If this is moving a parent into itself, that's an error
if vim.startswith(loop_action.dest_url, loop_action.src_url) then
error("Detected cycle in desired paths")
error('Detected cycle in desired paths')
end
-- We've detected a move cycle (e.g. MOVE /a -> /b + MOVE /b -> /a)
-- Split one of the moves and retry
local intermediate_url =
string.format("%s__oil_tmp_%05d", loop_action.src_url, math.random(999999))
string.format('%s__oil_tmp_%05d', loop_action.src_url, math.random(999999))
local move_1 = {
type = "move",
type = 'move',
entry_type = loop_action.entry_type,
src_url = loop_action.src_url,
dest_url = intermediate_url,
}
local move_2 = {
type = "move",
type = 'move',
entry_type = loop_action.entry_type,
src_url = intermediate_url,
dest_url = loop_action.dest_url,
@ -340,16 +340,16 @@ M.enforce_action_order = function(actions)
dest_trie:insert_action(move_1.dest_url, move_1)
src_trie:insert_action(move_1.src_url, move_1)
else
error("Detected cycle in desired paths")
error('Detected cycle in desired paths')
end
end
if selected then
if selected.type == "move" or selected.type == "copy" then
if vim.startswith(selected.dest_url, selected.src_url .. "/") then
if selected.type == 'move' or selected.type == 'copy' then
if vim.startswith(selected.dest_url, selected.src_url .. '/') then
error(
string.format(
"Cannot move or copy parent into itself: %s -> %s",
'Cannot move or copy parent into itself: %s -> %s',
selected.src_url,
selected.dest_url
)
@ -360,9 +360,9 @@ M.enforce_action_order = function(actions)
end
if to_remove then
if to_remove.type == "delete" or to_remove.type == "change" then
if to_remove.type == 'delete' or to_remove.type == 'change' then
src_trie:remove_action(to_remove.url, to_remove)
elseif to_remove.type == "create" then
elseif to_remove.type == 'create' then
dest_trie:remove_action(to_remove.url, to_remove)
else
dest_trie:remove_action(to_remove.dest_url, to_remove)
@ -387,8 +387,8 @@ local progress
---@param cb fun(err: nil|string)
M.process_actions = function(actions, cb)
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilActionsPre", modeline = false, data = { actions = actions } }
'User',
{ pattern = 'OilActionsPre', modeline = false, data = { actions = actions } }
)
local did_complete = nil
@ -398,14 +398,14 @@ M.process_actions = function(actions, cb)
-- Convert some cross-adapter moves to a copy + delete
for _, action in ipairs(actions) do
if action.type == "move" then
if action.type == 'move' then
local _, cross_action = util.get_adapter_for_action(action)
-- Only do the conversion if the cross-adapter support is "copy"
if cross_action == "copy" then
if cross_action == 'copy' then
---@diagnostic disable-next-line: assign-type-mismatch
action.type = "copy"
action.type = 'copy'
table.insert(actions, {
type = "delete",
type = 'delete',
url = action.src_url,
entry_type = action.entry_type,
})
@ -421,8 +421,8 @@ M.process_actions = function(actions, cb)
progress:close()
progress = nil
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilActionsPost", modeline = false, data = { err = err, actions = actions } }
'User',
{ pattern = 'OilActionsPost', modeline = false, data = { err = err, actions = actions } }
)
cb(err)
end
@ -435,7 +435,7 @@ M.process_actions = function(actions, cb)
-- TODO some actions are actually cancelable.
-- We should stop them instead of stopping after the current action
cancel = function()
finish("Canceled")
finish('Canceled')
end,
})
end
@ -472,7 +472,7 @@ M.process_actions = function(actions, cb)
next_action()
end
end)
if action.type == "change" then
if action.type == 'change' then
---@cast action oil.ChangeAction
columns.perform_change_action(adapter, action, callback)
else
@ -502,7 +502,7 @@ M.try_write_changes = function(confirm, cb)
cb = function(_err) end
end
if mutation_in_progress then
cb("Cannot perform mutation when already in progress")
cb('Cannot perform mutation when already in progress')
return
end
local current_buf = vim.api.nvim_get_current_buf()
@ -537,7 +537,7 @@ M.try_write_changes = function(confirm, cb)
mutation_in_progress = false
end
local ns = vim.api.nvim_create_namespace("Oil")
local ns = vim.api.nvim_create_namespace('Oil')
vim.diagnostic.reset(ns)
if not vim.tbl_isempty(all_errors) then
for bufnr, errors in pairs(all_errors) do
@ -564,7 +564,7 @@ M.try_write_changes = function(confirm, cb)
end)
end
unlock()
cb("Error parsing oil buffers")
cb('Error parsing oil buffers')
return
end
@ -572,7 +572,7 @@ M.try_write_changes = function(confirm, cb)
confirmation.show(actions, confirm, function(proceed)
if not proceed then
unlock()
cb("Canceled")
cb('Canceled')
return
end
@ -581,7 +581,7 @@ M.try_write_changes = function(confirm, cb)
vim.schedule_wrap(function(err)
view.unlock_buffers()
if err then
err = string.format("[oil] Error applying actions: %s", err)
err = string.format('[oil] Error applying actions: %s', err)
view.rerender_all_oil_buffers(nil, function()
cb(err)
end)
@ -591,13 +591,13 @@ M.try_write_changes = function(confirm, cb)
-- get the entry under the cursor and make sure the cursor stays on it
view.set_last_cursor(
vim.api.nvim_buf_get_name(0),
vim.split(current_entry.parsed_name or current_entry.name, "/")[1]
vim.split(current_entry.parsed_name or current_entry.name, '/')[1]
)
end
view.rerender_all_oil_buffers(nil, function(render_err)
vim.api.nvim_exec_autocmds(
"User",
{ pattern = "OilMutationComplete", modeline = false }
'User',
{ pattern = 'OilMutationComplete', modeline = false }
)
cb(render_err)
end)