-- rename_term.lua -- Vault-wide find-and-replace with a confirmation popup. -- Usage: require("rename_term").run() -- Keybinding: 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", "", 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