384 lines
11 KiB
Lua
Executable File
384 lines
11 KiB
Lua
Executable File
local api = vim.api
|
|
|
|
local buffer = require("dirbuf.buffer")
|
|
local config = require("dirbuf.config")
|
|
local fs = require("dirbuf.fs")
|
|
local planner = require("dirbuf.planner")
|
|
|
|
local M = {}
|
|
|
|
local CURRENT_BUFFER = 0
|
|
local CURRENT_WINDOW = 0
|
|
|
|
function M.setup(opts)
|
|
local errors = config.update(opts)
|
|
if #errors == 1 then
|
|
api.nvim_err_writeln("dirbuf.setup: " .. errors[1])
|
|
elseif #errors > 1 then
|
|
api.nvim_err_writeln("dirbuf.setup:")
|
|
for _, err in ipairs(errors) do
|
|
api.nvim_err_writeln(" " .. err)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- `normalize_path` takes a `path` entered by the user, potentially containing
|
|
-- duplicate path separators, "..", or trailing path separators, and ensures
|
|
-- that all duplicate path separators are removed, there is no trailing path
|
|
-- separator, and all ".."s are simplified. This does not resolve symlinks.
|
|
--
|
|
-- This exists to ensure that all paths are displayed in a consistent way and
|
|
-- to simplify path manipulation logic.
|
|
local function normalize_path(path)
|
|
path = vim.fn.simplify(vim.fn.fnamemodify(path, ":p"))
|
|
-- On Windows, simplify keeps the path_separator on directories
|
|
if path:sub(-1, -1) == fs.path_separator then
|
|
path = vim.fn.fnamemodify(path, ":h")
|
|
end
|
|
return path
|
|
end
|
|
|
|
-- `fill_dirbuf` fills the current buffer with the contents of its
|
|
-- corresponding directory. Note that the current buffer must have the name of
|
|
-- a valid directory.
|
|
--
|
|
-- If `on_fname` is set, then the cursor will be put on the line corresponding
|
|
-- to `on_fname`.
|
|
--
|
|
-- Returns: err
|
|
local function fill_dirbuf(on_fname)
|
|
local dir = api.nvim_buf_get_name(CURRENT_BUFFER)
|
|
local err, fs_entries = fs.get_fs_entries(dir, vim.b.dirbuf_show_hidden)
|
|
if err ~= nil then
|
|
return err
|
|
end
|
|
|
|
-- Before we set lines, we set undolevels to -1 so we delete the history when
|
|
-- we set the lines. This prevents people going back to now-invalid hashes
|
|
-- and potentially messing up their directory on accident
|
|
local buf_lines, fname_line = buffer.write_fs_entries(fs_entries, on_fname)
|
|
local undolevels = vim.bo.undolevels
|
|
vim.bo.undolevels = -1
|
|
api.nvim_buf_set_lines(CURRENT_BUFFER, 0, -1, true, buf_lines)
|
|
vim.bo.undolevels = undolevels
|
|
vim.b.dirbuf = fs_entries
|
|
|
|
vim.bo.tabstop = #"#" + buffer.HASH_LEN + config.get("hash_padding")
|
|
api.nvim_win_set_cursor(CURRENT_WINDOW, { fname_line or 1, #"#" + buffer.HASH_LEN + #"\t" })
|
|
vim.bo.modified = false
|
|
|
|
return nil
|
|
end
|
|
|
|
function M.init_dirbuf(history, history_index, update_history, from_path)
|
|
-- Preserve altbuf
|
|
local altbuf = vim.fn.bufnr("#")
|
|
|
|
local path = normalize_path(vim.fn.expand("%"))
|
|
api.nvim_buf_set_name(CURRENT_BUFFER, path)
|
|
|
|
-- Determine where to place cursor
|
|
-- We ignore errors in case the buffer is empty
|
|
local _, _, cursor_fname, _ = buffer.parse_line(api.nvim_get_current_line())
|
|
-- See if we're coming from a path below this dirbuf.
|
|
if from_path ~= nil and vim.startswith(from_path, path) then
|
|
-- Make sure we're clipping past the "/" in from_path
|
|
local fname_start = #path + 1
|
|
if path:sub(-1, -1) ~= fs.path_separator then
|
|
fname_start = fname_start + 1
|
|
end
|
|
local last_path_separator = from_path:find(fs.path_separator, fname_start, true)
|
|
if last_path_separator ~= nil then
|
|
cursor_fname = from_path:sub(fname_start, last_path_separator - 1)
|
|
else
|
|
cursor_fname = from_path:sub(fname_start)
|
|
end
|
|
end
|
|
|
|
-- Update history
|
|
if history == nil then
|
|
history = {}
|
|
history_index = 0
|
|
end
|
|
if update_history then
|
|
-- Clear old history
|
|
while #history > history_index do
|
|
table.remove(history)
|
|
end
|
|
-- We don't add to history if we're just refreshing the dirbuf
|
|
if path ~= history[history_index] then
|
|
table.insert(history, path)
|
|
history_index = history_index + 1
|
|
end
|
|
end
|
|
vim.b.dirbuf_history = history
|
|
vim.b.dirbuf_history_index = history_index
|
|
|
|
-- Set dirbuf options
|
|
vim.bo.filetype = "dirbuf"
|
|
vim.bo.buftype = "acwrite"
|
|
vim.bo.bufhidden = "wipe"
|
|
-- Normally unnecessary but sometimes other plugins make things unmodifiable,
|
|
-- so we have to do this to prevent running into errors in fill_dirbuf
|
|
vim.bo.modifiable = true
|
|
|
|
-- Set "dirbuf_show_hidden" to default if it is unset
|
|
if vim.b.dirbuf_show_hidden == nil then
|
|
vim.b.dirbuf_show_hidden = config.get("show_hidden")
|
|
end
|
|
|
|
if altbuf ~= -1 then
|
|
vim.fn.setreg("#", altbuf)
|
|
end
|
|
local err = fill_dirbuf(cursor_fname)
|
|
if err ~= nil then
|
|
api.nvim_err_writeln(err)
|
|
return
|
|
end
|
|
end
|
|
|
|
function M.get_cursor_path()
|
|
local err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
|
|
if err ~= nil then
|
|
error(err)
|
|
end
|
|
local dir = normalize_path(vim.fn.expand("%"))
|
|
return fs.join_paths(dir, fname)
|
|
end
|
|
|
|
-- If `path` is a file, this returns the absolute path to its parent. Otherwise
|
|
-- it returns the absolute path of `path`.
|
|
local function directify(path)
|
|
if fs.is_directory(path) then
|
|
return vim.fn.fnamemodify(path, ":p")
|
|
else
|
|
return vim.fn.fnamemodify(path, ":h:p")
|
|
end
|
|
end
|
|
|
|
function M.open(path)
|
|
if path == "" then
|
|
path = api.nvim_buf_get_name(CURRENT_BUFFER)
|
|
end
|
|
path = normalize_path(directify(path))
|
|
|
|
local from_path = normalize_path(vim.fn.expand("%"))
|
|
if from_path == path then
|
|
-- If we're not leaving, we want to keep the cursor on the same line
|
|
local err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
|
|
if err ~= nil then
|
|
api.nvim_err_writeln("Error placing cursor: " .. err)
|
|
return
|
|
end
|
|
from_path = fs.join_paths(path, fname)
|
|
end
|
|
|
|
local keepalt = ""
|
|
if vim.bo.filetype == "dirbuf" then
|
|
-- If we're leaving a dirbuf, keep our alternate buffer
|
|
keepalt = "keepalt"
|
|
end
|
|
local history, history_index = vim.b.dirbuf_history, vim.b.dirbuf_history_index
|
|
vim.cmd(keepalt .. " noautocmd edit " .. vim.fn.fnameescape(path))
|
|
-- Sanity check: If we're not in the file we just edited, something went
|
|
-- wrong. This can happen if someone has `:set nohidden confirm`,
|
|
-- accidentally opens dirbuf, and hits escape at the save prompt. The edit
|
|
-- "fails" without raising an error
|
|
if api.nvim_buf_get_name(CURRENT_BUFFER) ~= path then
|
|
return
|
|
end
|
|
M.init_dirbuf(history, history_index, true, from_path)
|
|
end
|
|
|
|
function M.enter(cmd)
|
|
if cmd == nil then
|
|
cmd = "edit"
|
|
end
|
|
|
|
if vim.bo.filetype ~= "dirbuf" then
|
|
api.nvim_err_writeln("Operation only supports 'filetype=dirbuf'")
|
|
return
|
|
end
|
|
|
|
local err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
|
|
if err ~= nil then
|
|
api.nvim_err_writeln(err)
|
|
return
|
|
end
|
|
if vim.bo.modified then
|
|
api.nvim_err_writeln(string.format("Cannot enter '%s'. Dirbuf must be saved first", fname))
|
|
return
|
|
end
|
|
|
|
local dir = normalize_path(vim.fn.expand("%"))
|
|
local path = fs.join_paths(dir, fname)
|
|
local noautocmd = ""
|
|
if fs.is_directory(path) then
|
|
noautocmd = "noautocmd"
|
|
end
|
|
local history, history_index = vim.b.dirbuf_history, vim.b.dirbuf_history_index
|
|
vim.cmd("keepalt " .. noautocmd .. " " .. cmd .. " " .. vim.fn.fnameescape(path))
|
|
if fs.is_directory(path) then
|
|
M.init_dirbuf(history, history_index, true)
|
|
end
|
|
end
|
|
|
|
function M.jump_history(n)
|
|
if vim.bo.filetype ~= "dirbuf" then
|
|
api.nvim_err_writeln("Operation only supports 'filetype=dirbuf'")
|
|
return
|
|
end
|
|
local history, history_index = vim.b.dirbuf_history, vim.b.dirbuf_history_index
|
|
local next_index = math.max(1, math.min(#history, history_index + n))
|
|
vim.cmd("keepalt noautocmd edit " .. vim.fn.fnameescape(history[next_index]))
|
|
M.init_dirbuf(history, next_index, false, history[history_index])
|
|
end
|
|
|
|
function M.quit()
|
|
if vim.bo.filetype ~= "dirbuf" then
|
|
api.nvim_err_writeln(":DirbufQuit only supports 'filetype=dirbuf'")
|
|
return
|
|
end
|
|
|
|
local altbuf = vim.fn.bufnr("#")
|
|
if altbuf == -1 or altbuf == api.nvim_get_current_buf() then
|
|
vim.cmd("bdelete")
|
|
else
|
|
api.nvim_set_current_buf(altbuf)
|
|
end
|
|
end
|
|
|
|
-- Ensure that the directory has not changed since our last snapshot
|
|
local function check_dirbuf(buf)
|
|
local dir = api.nvim_buf_get_name(buf)
|
|
local err, current_fs_entries = fs.get_fs_entries(dir, vim.b.dirbuf_show_hidden)
|
|
if err ~= nil then
|
|
return "Error while checking: " .. err
|
|
end
|
|
|
|
if not vim.deep_equal(vim.b.dirbuf, current_fs_entries) then
|
|
return "Snapshot out of date with current directory. Run :edit! to refresh"
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- print_plan() should only be called from dirbuf.sync()
|
|
local function print_plan(plan)
|
|
local function fmt_fs_entry(fs_entry)
|
|
return vim.fn.shellescape(buffer.display_fs_entry(fs_entry))
|
|
end
|
|
|
|
for _, action in ipairs(plan) do
|
|
if action.type == "create" then
|
|
if action.fs_entry.ftype == "directory" then
|
|
print("mkdir " .. fmt_fs_entry(action.fs_entry))
|
|
else
|
|
print("touch " .. fmt_fs_entry(action.fs_entry))
|
|
end
|
|
elseif action.type == "copy" then
|
|
print("cp " .. fmt_fs_entry(action.src_fs_entry) .. " " .. fmt_fs_entry(action.dst_fs_entry))
|
|
elseif action.type == "delete" then
|
|
print("rm " .. fmt_fs_entry(action.fs_entry))
|
|
elseif action.type == "move" then
|
|
print("mv " .. fmt_fs_entry(action.src_fs_entry) .. " " .. fmt_fs_entry(action.dst_fs_entry))
|
|
else
|
|
error("Unrecognized action: " .. vim.inspect(action))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- do_plan() should only be called from dirbuf.sync()
|
|
local function do_plan(plan)
|
|
local err = planner.execute_plan(plan)
|
|
if err ~= nil then
|
|
api.nvim_err_writeln("Error making changes: " .. err)
|
|
api.nvim_err_writeln("WARNING: Dirbuf in inconsistent state. Run :edit! to refresh")
|
|
return
|
|
end
|
|
|
|
-- Leave cursor on the same file
|
|
local fname
|
|
err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
|
|
if err ~= nil then
|
|
api.nvim_err_writeln(err)
|
|
return
|
|
end
|
|
err = fill_dirbuf(fname)
|
|
if err ~= nil then
|
|
api.nvim_err_writeln(err)
|
|
return
|
|
end
|
|
end
|
|
|
|
function M.sync(opt)
|
|
if opt == nil then
|
|
opt = ""
|
|
end
|
|
|
|
if vim.bo.filetype ~= "dirbuf" then
|
|
api.nvim_err_writeln(":DirbufSync only supports 'filetype=dirbuf'")
|
|
return
|
|
end
|
|
|
|
if opt ~= "" and opt ~= "-confirm" and opt ~= "-dry-run" then
|
|
api.nvim_err_writeln(":DirbufSync unrecognized option: " .. opt)
|
|
end
|
|
|
|
if not vim.bo.modified then
|
|
return
|
|
end
|
|
|
|
local err = check_dirbuf(CURRENT_BUFFER)
|
|
if err ~= nil then
|
|
api.nvim_err_writeln("Cannot save dirbuf: " .. err)
|
|
return
|
|
end
|
|
|
|
local dir = api.nvim_buf_get_name(CURRENT_BUFFER)
|
|
local lines = api.nvim_buf_get_lines(CURRENT_BUFFER, 0, -1, true)
|
|
local changes
|
|
err, changes = planner.build_changes(dir, vim.b.dirbuf, lines)
|
|
if err ~= nil then
|
|
api.nvim_err_writeln(err)
|
|
return
|
|
end
|
|
|
|
local plan = planner.determine_plan(changes)
|
|
|
|
if opt == "-confirm" then
|
|
print_plan(plan)
|
|
-- We pcall to make Ctrl-C work
|
|
local ok, response = pcall(vim.fn.confirm, "Sync changes?", "&Yes\n&No", 2)
|
|
if ok and response == 1 then
|
|
do_plan(plan)
|
|
end
|
|
elseif opt == "-dry-run" then
|
|
print_plan(plan)
|
|
else
|
|
do_plan(plan)
|
|
end
|
|
end
|
|
|
|
function M.toggle_hide()
|
|
if vim.bo.filetype ~= "dirbuf" then
|
|
api.nvim_err_writeln("Operation only supports 'filetype=dirbuf'")
|
|
return
|
|
end
|
|
|
|
vim.b.dirbuf_show_hidden = not vim.b.dirbuf_show_hidden
|
|
-- Leave cursor on the same file
|
|
local err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
|
|
if err ~= nil then
|
|
api.nvim_err_writeln(err)
|
|
return
|
|
end
|
|
err = fill_dirbuf(fname)
|
|
if err ~= nil then
|
|
api.nvim_err_writeln(err)
|
|
return
|
|
end
|
|
end
|
|
|
|
return M
|