pages/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/planner.lua

214 lines
6.2 KiB
Lua
Executable File

local buffer = require("dirbuf.buffer")
local fs = require("dirbuf.fs")
local FSEntry = fs.FSEntry
local create, copy, delete, move = fs.plan.create, fs.plan.copy, fs.plan.delete, fs.plan.move
local M = {}
--[[
local record Changes
new_files: {FSEntry},
change_map: {string: Change},
}
local record Change
{FSEntry} -- dst_fs_entries
current_fs_entry: FSEntry
stays: bool
progress: Progress
end
local enum Progress
"unhandled"
"handling"
"handled"
end
--]]
-- `build_changes` creates a diff between the snapshotted state of the
-- directory buffer `dirbuf` and the updated state of the directory buffer
-- `lines`.
--
-- TODO: It's kinda gross that I just store `lines` because then I have to deal
-- with parsing here, but I'm not sure of a better way to do it
--
-- Returns: err, changes
function M.build_changes(dir, fs_entries, lines)
local new_files = {}
local change_map = {}
for _, fs_entry in pairs(fs_entries) do
change_map[fs_entry.fname] = {
current_fs_entry = fs_entry,
stays = false,
handled = false,
}
end
-- No duplicate fnames
local used_fnames = {}
for lnum, line in ipairs(lines) do
local err, hash, fname, ftype = buffer.parse_line(line)
if err ~= nil then
return string.format("Line %d: %s", lnum, err)
end
if fname == nil then
goto continue
end
if used_fnames[fname] ~= nil then
return string.format("Line %d: Duplicate name '%s'", lnum, fname)
end
local dst_fs_entry = FSEntry.new(fname, dir, ftype)
if hash == nil then
table.insert(new_files, dst_fs_entry)
else
local current_fs_entry = fs_entries[hash]
if current_fs_entry.ftype ~= dst_fs_entry.ftype then
return string.format("line %d: cannot change %s -> %s", lnum, current_fs_entry.ftype, dst_fs_entry.ftype)
end
if current_fs_entry.fname == dst_fs_entry.fname then
change_map[current_fs_entry.fname].stays = true
else
table.insert(change_map[current_fs_entry.fname], dst_fs_entry)
end
end
used_fnames[dst_fs_entry.fname] = true
::continue::
end
return nil, { change_map = change_map, new_files = new_files }
end
-- TODO: Currently we don't always find the optimal unsticking point
-- Also, sorry this is hard to read...
local function resolve_change(plan, change_map, change)
if change.progress == "handled" then
return
elseif change.progress == "handling" then
error("unhandled cycle detected")
end
change.progress = "handling"
-- If there's a cycle, we need to "unstick" it by moving one file to a
-- temporary location. However, we need to remember to move that temporary
-- file back to where we want after everything else in the cycle has been
-- resolved.
--
-- It's not obvious that we can get away with only returning one action.
-- However, due to our guarantee that the `Changes` we're given only use each
-- `fname` once (i.e. the max in-degree of the graph of filename changes is
-- 1), we know that we can only ever have one cycle from any given starting
-- point.
local post_resolution_action = nil
-- If the file doesn't stay, we prevent an extra copy by moving the file
-- as the last change. We arbitrarily pick the first file to move it after
-- everything
local move_to = nil
local stuck_fs_entry = nil
for _, dst_fs_entry in ipairs(change) do
local dependent_change = change_map[dst_fs_entry.fname]
if dependent_change ~= nil then
if dependent_change.progress == "handling" then
-- We have a cycle, we need to unstick it
if stuck_fs_entry ~= nil then
error("my assumption about `stuck_change` was wrong")
end
-- We handle this later
stuck_fs_entry = dst_fs_entry
goto continue
else
-- We can handle the dependent_change directly
-- Double check that my assumption holds
local rtn = resolve_change(plan, change_map, dependent_change)
if rtn ~= nil and post_resolution_action ~= nil then
error("my assumption about `post_resolution_action` was wrong")
end
post_resolution_action = rtn
end
end
if not change.stays and move_to == nil then
move_to = dst_fs_entry
else
table.insert(plan, copy(change.current_fs_entry, dst_fs_entry))
end
::continue::
end
local gone = false
if move_to ~= nil then
table.insert(plan, move(change.current_fs_entry, move_to))
gone = true
end
if stuck_fs_entry ~= nil then
if move_to ~= nil then
-- We have a safe place to copy from
post_resolution_action = copy(move_to, stuck_fs_entry)
elseif change.stays then
-- We have a safe place to copy from
post_resolution_action = copy(change.current_fs_entry, stuck_fs_entry)
else
-- We have NO safe place to copy from and we don't stay, so move to a
-- temporary and then move again
local temp_fs_entry = FSEntry.temp(change.current_fs_entry.ftype)
table.insert(plan, move(change.current_fs_entry, temp_fs_entry))
post_resolution_action = move(temp_fs_entry, stuck_fs_entry)
gone = true
end
end
-- The file gets deleted and we never moved it, so we have to directly delete
-- it
if not change.stays and not gone then
table.insert(plan, delete(change.current_fs_entry))
end
change.progress = "handled"
return post_resolution_action
end
-- `determine_plan` finds the most efficient sequence of actions necessary to
-- apply the set of validated changes we have `changes`.
--
-- Returns: list of actions as in fs.plan
function M.determine_plan(changes)
local plan = {}
for _, change in pairs(changes.change_map) do
local extra_action = resolve_change(plan, changes.change_map, change)
if extra_action ~= nil then
table.insert(plan, extra_action)
end
end
for _, fs_entry in ipairs(changes.new_files) do
table.insert(plan, create(fs_entry))
end
return plan
end
-- `execute_plan` executes the plan (i.e. sequence of actions) as created by
-- `determine_plan` using the `fs.actions` action handlers.
--
-- Returns: err
function M.execute_plan(plan)
-- TODO: Make this async
for _, action in ipairs(plan) do
local err = fs.actions[action.type](action)
if err ~= nil then
return err
end
end
return nil
end
return M