- add telescope_tasks.lua: vault-wide task pickers (<leader>tl / <leader>tL) - track mappings.lua and rename_term.lua in dotfiles - update CLAUDE.md: add vault definition, telescope_tasks entry, nvim lua file list - fix tdo/tdop aliases and ga/gx aliases in .bashrc - install_nvchad.sh: symlink personal nvim modules after clone Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
170 lines
4.7 KiB
Lua
170 lines
4.7 KiB
Lua
-- rename_term.lua
|
|
-- Vault-wide find-and-replace with a confirmation popup.
|
|
-- Usage: require("rename_term").run()
|
|
-- Keybinding: <leader>rn
|
|
|
|
local M = {}
|
|
|
|
local function get_vault_root()
|
|
-- prefer obsidian vault root, fall back to cwd
|
|
local ok, obs = pcall(require, "obsidian")
|
|
if ok and obs.get_client then
|
|
local client = obs.get_client()
|
|
if client and client.dir then
|
|
return tostring(client.dir)
|
|
end
|
|
end
|
|
return vim.fn.getcwd()
|
|
end
|
|
|
|
local function show_popup(lines, on_confirm)
|
|
local width = math.min(80, vim.o.columns - 4)
|
|
local height = math.min(#lines + 4, vim.o.lines - 6)
|
|
local row = math.floor((vim.o.lines - height) / 2)
|
|
local col = math.floor((vim.o.columns - width) / 2)
|
|
|
|
local buf = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
|
|
|
|
local display = { "Affected files — press y to apply, n/q to cancel:", "" }
|
|
for _, l in ipairs(lines) do
|
|
table.insert(display, " " .. l)
|
|
end
|
|
table.insert(display, "")
|
|
table.insert(display, string.format(" %d file(s) will be modified.", #lines))
|
|
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, display)
|
|
vim.api.nvim_buf_set_option(buf, "modifiable", false)
|
|
|
|
local win = vim.api.nvim_open_win(buf, true, {
|
|
relative = "editor",
|
|
width = width,
|
|
height = height,
|
|
row = row,
|
|
col = col,
|
|
style = "minimal",
|
|
border = "rounded",
|
|
})
|
|
|
|
local function close()
|
|
if vim.api.nvim_win_is_valid(win) then
|
|
vim.api.nvim_win_close(win, true)
|
|
end
|
|
end
|
|
|
|
local opts = { buffer = buf, nowait = true, silent = true }
|
|
|
|
vim.keymap.set("n", "y", function()
|
|
close()
|
|
on_confirm()
|
|
end, opts)
|
|
|
|
vim.keymap.set("n", "n", close, opts)
|
|
vim.keymap.set("n", "q", close, opts)
|
|
vim.keymap.set("n", "<Esc>", close, opts)
|
|
end
|
|
|
|
function M.run(search, replace)
|
|
local vault = get_vault_root()
|
|
|
|
-- prompt if not passed as args
|
|
if not search or search == "" then
|
|
search = vim.fn.input("Search: ")
|
|
if search == "" then
|
|
print("rename_term: search term is empty, aborting.")
|
|
return
|
|
end
|
|
end
|
|
if not replace or replace == "" then
|
|
replace = vim.fn.input("Replace: ")
|
|
-- allow replacing with empty string intentionally
|
|
end
|
|
|
|
-- find affected files (content)
|
|
local raw = vim.fn.systemlist(
|
|
string.format("grep -rl --include='*.md' -F %q %q", search, vault)
|
|
)
|
|
|
|
-- find affected file names
|
|
local named = vim.fn.systemlist(
|
|
string.format("find %q -name %q", vault, "*" .. search .. "*")
|
|
)
|
|
|
|
-- deduplicate
|
|
local seen = {}
|
|
local content_files = {}
|
|
for _, f in ipairs(raw) do
|
|
if not seen[f] then
|
|
seen[f] = true
|
|
table.insert(content_files, f)
|
|
end
|
|
end
|
|
|
|
if #content_files == 0 and #named == 0 then
|
|
vim.notify('rename_term: no matches for "' .. search .. '"', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
|
|
-- build display list
|
|
local display = {}
|
|
for _, f in ipairs(content_files) do
|
|
table.insert(display, vim.fn.fnamemodify(f, ":~:.") .. " [content]")
|
|
end
|
|
for _, f in ipairs(named) do
|
|
table.insert(display, vim.fn.fnamemodify(f, ":~:.") .. " [filename]")
|
|
end
|
|
|
|
show_popup(display, function()
|
|
local errors = {}
|
|
|
|
-- 1. replace content in all matched files
|
|
for _, f in ipairs(content_files) do
|
|
-- escape for sed: & / \ need escaping
|
|
local function sed_escape(s)
|
|
return s:gsub("([&/\\])", "\\%1")
|
|
end
|
|
local cmd = string.format(
|
|
"sed -i 's/%s/%s/g' %q",
|
|
sed_escape(search), sed_escape(replace), f
|
|
)
|
|
local result = vim.fn.system(cmd)
|
|
if vim.v.shell_error ~= 0 then
|
|
table.insert(errors, "content: " .. f .. " — " .. result)
|
|
end
|
|
end
|
|
|
|
-- 2. rename files whose names contain the search term
|
|
for _, f in ipairs(named) do
|
|
local dir = vim.fn.fnamemodify(f, ":h")
|
|
local base = vim.fn.fnamemodify(f, ":t")
|
|
local newbase = base:gsub(vim.pesc(search), replace)
|
|
local newpath = dir .. "/" .. newbase
|
|
if newbase ~= base then
|
|
local ok, err = os.rename(f, newpath)
|
|
if not ok then
|
|
table.insert(errors, "rename: " .. f .. " — " .. (err or "unknown"))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- reload any open buffers whose files were touched
|
|
for _, f in ipairs(content_files) do
|
|
local bufnr = vim.fn.bufnr(f)
|
|
if bufnr ~= -1 and vim.api.nvim_buf_is_loaded(bufnr) then
|
|
vim.api.nvim_buf_call(bufnr, function() vim.cmd("edit!") end)
|
|
end
|
|
end
|
|
|
|
if #errors > 0 then
|
|
vim.notify("rename_term errors:\n" .. table.concat(errors, "\n"), vim.log.levels.ERROR)
|
|
else
|
|
vim.notify(string.format(
|
|
'rename_term: replaced "%s" → "%s" in %d file(s), renamed %d file(s).',
|
|
search, replace, #content_files, #named
|
|
), vim.log.levels.INFO)
|
|
end
|
|
end)
|
|
end
|
|
|
|
return M
|