ppd.nvim/lua/ppd/dirstack.lua
2024-07-06 06:17:55 -05:00

185 lines
4.9 KiB
Lua

local config = require("ppd.config")
---@alias ppd.Dirstack.scope "global" | "tab" | "window"
---@class ppd.DirStack
---@field scope ppd.Dirstack.scope
---@field autocmd integer?
---@field watch_cd_enabled boolean
---@field augroup integer
---@field stack string[]
local DirStack = {}
--- Create a new Dirstack
---@param scope ppd.Dirstack.scope
---@return ppd.DirStack
function DirStack.new(scope)
local valid_scopes = {
"global",
"tab",
"window",
}
vim.validate {
scope = {
scope,
function(sc)
return vim.tbl_contains(valid_scopes, sc)
end,
"a valid DirStack scope",
},
}
local self = setmetatable({
scope = scope,
stack = {},
augroup = vim.api.nvim_create_augroup("Ppd" .. scope:upper(), { clear = false }),
}, { __index = DirStack })
-- Put at minimum, a single directory on the stack by default so it's possible to unwind to the
if scope == "global" then
self:push(vim.fn.getcwd(-1, -1))
elseif scope == "tab" then
self:push(vim.fn.getcwd(-1, 0))
else
self:push(vim.fn.getcwd(0))
end
-- Start the watch *after* we have a single base directory on the path
self:watch_cd()
return self
end
--- Creates an autocmd if not already called to watch cd events for the
--- relevant scope and push the directories onto the stack if `auto_cd` is
--- enabled
function DirStack:watch_cd()
self.watch_cd_enabled = true
if self.autocmd then
return
end
self.autocmd = vim.api.nvim_create_autocmd("DirChanged", {
group = self.augroup,
desc = "Ppd " .. self.scope .. " Listener for Auto CD",
pattern = self.scope,
callback = function(args)
if config.auto_cd and self.watch_cd_enabled then
local new_dir = args.file
self:push(new_dir)
end
end,
})
end
function DirStack:unwatch_cd()
self.watch_cd_enabled = false
end
--- Get the string representation of the stack
---@return string
function DirStack:repr()
return table.concat(self.stack, " ")
end
--- Removes one or more directories off the stack and returns them
---@private
---@param count integer? The number of directories to pop or nil for one
---@return string[] removed_paths The paths removed in order from the stack
function DirStack:pop(count)
count = count or 1
local removed_paths = {}
while not self:empty() and count > 0 do
count = count - 1
table.insert(removed_paths, table.remove(self.stack))
end
return removed_paths
end
--- Get whether the stack is empty, empty being defined as only one element
---@return boolean is_empty true if the stack is empty
function DirStack:empty()
return #self.stack == 1
end
--- Push a path onto the stack
---@private
---@param path string
function DirStack:push(path)
path = vim.fn.fnamemodify(vim.fn.fnamemodify(path, ":p:h"), ":~")
if not ((config.dedup.all and self:check_dup_all(path)) or (config.dedup.top and self:check_dup_top(path))) then
table.insert(self.stack, path)
end
end
--- Peek the top of the stack
---@return string? top_path The path at the top of the stack if it exists
function DirStack:peek()
return self.stack[#self.stack]
end
--- Changes directory accounting for the current scope
---@private
---@param path string A path to change the directory to
function DirStack:cd(path)
local cmd = ({
["global"] = vim.cmd.cd,
["tab"] = vim.cmd.tcd,
["window"] = vim.cmd.lcd,
})[self.scope]
self:unwatch_cd()
cmd(path)
self:watch_cd()
end
--- Checks if the topmost element in the stack is the same as the given path
---@param path string
---@return boolean dup_found true if the topmost element is the same as the given path
function DirStack:check_dup_top(path)
return self:peek() == path
end
--- Checks if any element within the current stack is the same as the given path
---@param path string
---@return boolean dup_found true if a duplicate value is located in the stack
function DirStack:check_dup_all(path)
return vim.iter(self.stack):any(function(val)
return val == path
end)
end
--- Change the current directory for the relevant scope to the given path and push it onto the stack
---@param path string
function DirStack:pushd(path)
if not path or path:len() == 0 then
vim.notify(self:repr())
return
end
self:cd(path)
self:push(path)
if config.notify.on_pushd then
vim.notify(self:repr())
end
end
--- Removes one or more directories off the stack and changes the path to it
---@param count integer? The number of directories to pop or nil for one
function DirStack:popd(count)
count = count or 1
self:pop(count)
local top_path = self:peek()
if top_path then
self:cd(top_path)
end
if config.notify.on_popd then
vim.notify(self:repr())
end
end
return DirStack