mirror of
https://github.com/tiyn/file-renamer.nvim.git
synced 2026-04-17 02:54:49 +02:00
1.1.0: Added Icons
This commit is contained in:
14
README.md
14
README.md
@@ -12,17 +12,21 @@ it.
|
||||
## Features
|
||||
|
||||
- Edit filenames in a buffer
|
||||
- Navigate directories (`<CR>`)
|
||||
- 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 `<CR>` 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" },
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", "<CR>", 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user