From da78d496ffb3dd55a4c70b4a9c28686630a57d3a Mon Sep 17 00:00:00 2001 From: tiyn Date: Mon, 6 Apr 2026 05:20:37 +0200 Subject: [PATCH] 1.1.0: Added Icons --- README.md | 14 +-- doc/file-renamer.txt | 5 ++ lua/file-renamer/init.lua | 177 ++++++++++++++++++++++++++++++++------ 3 files changed, 165 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 003d58c..d02e648 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,21 @@ it. ## Features - Edit filenames in a buffer -- Navigate directories (``) -- Support for nested paths (`file -> dir/file`) -- Automatically creates missing directories -- Safe renaming (handles swaps like `a <-> b`) -- Skips invalid or conflicting renames +- Navigate directories (press `` on directory) +- Support for nested renaming (for `file -> dir/file` a directory is created if it doesn't exist + already) +- Safe renaming and directory creation (handles `a <-> b` and `a -> a/a`) +- Skips invalid or conflicting renames (prohibits collision of file and directory names) ## Installation (lazy.nvim) ```lua return { "tiyn/file-renamer.nvim", + dependencies = { + "nvim-tree/nvim-web-devicons", + }, + cmd = { "Ren", "Renamer" }, } ```` diff --git a/doc/file-renamer.txt b/doc/file-renamer.txt index 2cffd78..cce613b 100644 --- a/doc/file-renamer.txt +++ b/doc/file-renamer.txt @@ -108,6 +108,7 @@ LIMITATIONS *file-renamer-limitations* {{{1 - No per-file highlighting of modified names (planned) CHANGELOG *file-renamer-changelog* {{{1 + 1.0.0 - Initial release - Basic rename buffer - Directory navigation @@ -116,3 +117,7 @@ CHANGELOG *file-renamer-changelog* {{{1 - Directory creation support - Conflict detection and skipping - Rollback on failure + +1.0.1 - Fixed a -> a/a renaming bug + +1.1.0 - Added Icons diff --git a/lua/file-renamer/init.lua b/lua/file-renamer/init.lua index bec8193..8e6a928 100644 --- a/lua/file-renamer/init.lua +++ b/lua/file-renamer/init.lua @@ -6,9 +6,105 @@ local header = { "Renamer: change names then give command :Ren\n", } +local has_devicons, devicons = pcall(require, "nvim-web-devicons") +local ns = vim.api.nvim_create_namespace("renamer_icons") + +local function strip_indent(line) + return (line:gsub("^%s+", "")) +end + +local function add_indent(line) + return " " .. line +end + +local function get_file_icon(name) + if not has_devicons then + return "", "Normal" + end + + local icon, hl = devicons.get_icon(name, nil, { default = true }) + return icon or "", hl or "Normal" +end + +local function is_editable_entry_line(line) + return line ~= "" and not line:match("^#") +end + +local function clear_icons(buf) + vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) +end + +local function render_icons(buf) + clear_icons(buf) + + if not has_devicons or not vim.api.nvim_buf_is_valid(buf) then + return + end + + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + + for i, line in ipairs(lines) do + if is_editable_entry_line(line) then + local clean = strip_indent(vim.trim(line)) + local icon, hl = "", "Normal" + + if clean:sub(-1) == "/" then + icon, hl = "", "Directory" + else + icon, hl = get_file_icon(clean) + end + + if icon ~= "" then + vim.api.nvim_buf_set_extmark(buf, ns, i - 1, 0, { + virt_text = { { icon .. " ", hl } }, + virt_text_win_col = 0, + }) + end + end + end +end + +local function setup_autocmd(buf) + local group = vim.api.nvim_create_augroup("RenamerIcons_" .. buf, { clear = true }) + + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "InsertLeave" }, { + group = group, + buffer = buf, + callback = function() + if vim.api.nvim_buf_is_valid(buf) then + render_icons(buf) + end + end, + }) + + vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { + group = group, + buffer = buf, + callback = function() + local pos = vim.api.nvim_win_get_cursor(0) + local row, col = pos[1], pos[2] + + local line = vim.api.nvim_get_current_line() + + if is_editable_entry_line(line) and col < 2 then + vim.api.nvim_win_set_cursor(0, { row, 2 }) + end + end, + }) + + vim.api.nvim_create_autocmd("BufWipeout", { + group = group, + buffer = buf, + callback = function() + pcall(vim.api.nvim_del_augroup_by_id, group) + end, + }) +end + function M.start(start_dir, cursor_line) vim.cmd("enew") + local buf = vim.api.nvim_get_current_buf() local cwd = start_dir or vim.fn.getcwd() vim.b.rename_cwd = cwd @@ -31,8 +127,12 @@ function M.start(start_dir, cursor_line) table.sort(normal_files) local ordered = {} - for _, d in ipairs(dirs) do ordered[#ordered + 1] = d end - for _, f in ipairs(normal_files) do ordered[#ordered + 1] = f end + for _, d in ipairs(dirs) do + ordered[#ordered + 1] = d + end + for _, f in ipairs(normal_files) do + ordered[#ordered + 1] = f + end vim.b.rename_original_names = ordered @@ -46,10 +146,14 @@ function M.start(start_dir, cursor_line) lines[#lines + 1] = hashes .. "Currently editing: " .. cwd .. "/*" lines[#lines + 1] = "# ../" - for _, d in ipairs(dirs) do lines[#lines + 1] = d .. "/" end - for _, f in ipairs(normal_files) do lines[#lines + 1] = f end + for _, d in ipairs(dirs) do + lines[#lines + 1] = add_indent(d .. "/") + end + for _, f in ipairs(normal_files) do + lines[#lines + 1] = add_indent(f) + end - vim.api.nvim_buf_set_lines(0, 0, -1, false, lines) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) vim.bo.buftype = "nofile" vim.bo.bufhidden = "wipe" @@ -57,16 +161,25 @@ function M.start(start_dir, cursor_line) vim.keymap.set("n", "", M.enter, { buffer = true, silent = true }) - if cursor_line then - vim.api.nvim_win_set_cursor(0, { cursor_line, 0 }) - else - vim.api.nvim_win_set_cursor(0, { #header + 3, 0 }) + setup_autocmd(buf) + render_icons(buf) + + local line_count = vim.api.nvim_buf_line_count(buf) + local target_line = cursor_line or (#header + 3) + + if target_line < 1 then + target_line = 1 end + if target_line > line_count then + target_line = line_count + end + + vim.api.nvim_win_set_cursor(0, { target_line, 2 }) vim.cmd("syntax clear") vim.cmd([[syntax match RenameComment "^#.*"]]) - vim.cmd([[syntax match RenameDirectory "^[^#].*/$"]]) - vim.cmd([[syntax match RenameFile "^[^#].*[^/]$"]]) + vim.cmd([[syntax match RenameDirectory "^\s*[^#].*/$"]]) + vim.cmd([[syntax match RenameFile "^\s*[^#].*[^/]$"]]) vim.cmd("highlight default link RenameComment Comment") vim.cmd("highlight default link RenameDirectory Constant") @@ -74,6 +187,7 @@ function M.start(start_dir, cursor_line) end function M.restore_buffer() + local buf = vim.api.nvim_get_current_buf() local cwd = vim.b.rename_cwd local original = vim.b.rename_original_names or {} @@ -101,10 +215,15 @@ function M.restore_buffer() lines[#lines + 1] = hashes .. "Currently editing: " .. cwd .. "/*" lines[#lines + 1] = "# ../" - for _, d in ipairs(dirs) do lines[#lines + 1] = d .. "/" end - for _, f in ipairs(files) do lines[#lines + 1] = f end + for _, d in ipairs(dirs) do + lines[#lines + 1] = add_indent(d .. "/") + end + for _, f in ipairs(files) do + lines[#lines + 1] = add_indent(f) + end - vim.api.nvim_buf_set_lines(0, 0, -1, false, lines) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + render_icons(buf) end function M.enter() @@ -116,8 +235,10 @@ function M.enter() return end - if line:sub(-1) == "/" then - M.start(cwd .. "/" .. line:gsub("/$", "")) + local clean = strip_indent(line) + + if clean:sub(-1) == "/" then + M.start(cwd .. "/" .. clean:gsub("/$", "")) end end @@ -128,7 +249,8 @@ function M.perform_rename() local new_names = {} for _, line in ipairs(lines) do if not line:match("^#") and line ~= "" then - new_names[#new_names + 1] = vim.trim(line):gsub("/$", "") + local clean = strip_indent(vim.trim(line)):gsub("/$", "") + new_names[#new_names + 1] = clean end end @@ -145,9 +267,12 @@ function M.perform_rename() target_count[new] = (target_count[new] or 0) + 1 end - local target_set = {} - for _, new in ipairs(new_names) do - target_set[vim.fn.simplify(cwd .. "/" .. new)] = true + local moving_old_paths = {} + for i, old in ipairs(old_names) do + local new = new_names[i] + if old ~= new then + moving_old_paths[vim.fn.simplify(cwd .. "/" .. old)] = true + end end local ops = {} @@ -158,21 +283,22 @@ function M.perform_rename() if old ~= new then if target_count[new] ~= 1 then print("Skipping duplicate target:", new) - else + local old_path = vim.fn.simplify(cwd .. "/" .. old) local new_path = vim.fn.simplify(cwd .. "/" .. new) - local exists = vim.fn.filereadable(new_path) == 1 - or vim.fn.isdirectory(new_path) == 1 + local exists = vim.fn.filereadable(new_path) == 1 or vim.fn.isdirectory(new_path) == 1 - if exists and not target_set[new_path] then + local occupied_by_moving_source = moving_old_paths[new_path] == true + + if exists and not occupied_by_moving_source then print("Target exists, skipping:", new) else ops[#ops + 1] = { index = i, old = old, new = new, - old_path = cwd .. "/" .. old, + old_path = old_path, new_path = new_path, } end @@ -202,7 +328,6 @@ function M.perform_rename() for _, op in ipairs(ops) do if not op.failed then local ok = os.rename(op.tmp, op.new_path) - if not ok then print("Failed final rename:", op.new)