Skip to content

refactor(actions): common code for rename-file #2550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/nvim-tree-lua.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,13 @@ fs.rename_sub({node}) *nvim-tree-api.fs.rename_sub()*
Parameters: ~
• {node} (Node) file or folder

fs.rename_relative({node}) *nvim-tree-api.fs.rename_relative()*

Prompt to rename a file or folder by relative path.

Parameters: ~
• {node} (Node) file or folder

fs.rename_full({node}) *nvim-tree-api.fs.rename_full()*
Prompt to rename a file or folder by absolute path.

Expand Down
127 changes: 63 additions & 64 deletions lua/nvim-tree/actions/fs/rename-file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,102 +2,101 @@ local lib = require "nvim-tree.lib"
local utils = require "nvim-tree.utils"
local events = require "nvim-tree.events"
local notify = require "nvim-tree.notify"
local utils_ui = require "nvim-tree.utils-ui"

local find_file = require("nvim-tree.actions.finders.find-file").fn

local M = {
config = {},
}

local ALLOWED_MODIFIERS = {
[":p"] = true,
[":p:h"] = true,
[":t"] = true,
[":t:r"] = true,
}

local function err_fmt(from, to, reason)
return string.format("Cannot rename %s -> %s: %s", from, to, reason)
end

function M.rename(node, to)
--- note: this function is used elsewhere
--- @param node table
--- @param path string path destination
function M.rename_node_to(node, path)
local notify_from = notify.render_path(node.absolute_path)
local notify_to = notify.render_path(to)
local notify_to = notify.render_path(path)

if utils.file_exists(to) then
if utils.file_exists(path) then
notify.warn(err_fmt(notify_from, notify_to, "file already exists"))
return
end

events._dispatch_will_rename_node(node.absolute_path, to)
local success, err = vim.loop.fs_rename(node.absolute_path, to)
events._dispatch_will_rename_node(node.absolute_path, path)
local success, err = vim.loop.fs_rename(node.absolute_path, path)
if not success then
return notify.warn(err_fmt(notify_from, notify_to, err))
end
notify.info(string.format("%s -> %s", notify_from, notify_to))
utils.rename_loaded_buffers(node.absolute_path, to)
events._dispatch_node_renamed(node.absolute_path, to)
utils.rename_loaded_buffers(node.absolute_path, path)
events._dispatch_node_renamed(node.absolute_path, path)
end

function M.fn(default_modifier)
default_modifier = default_modifier or ":t"
--- @class fsPromptForRenameOpts: InputPathEditorOpts

return function(node, modifier)
if type(node) ~= "table" then
node = lib.get_node_at_cursor()
end
--- @param opts? fsPromptForRenameOpts
function M.prompt_for_rename(node, opts)
if type(node) ~= "table" then
node = lib.get_node_at_cursor()
end

if type(modifier) ~= "string" then
modifier = default_modifier
end
local opts_default = { absolute = true }
if type(opts) ~= "table" then
opts = opts_default
end

-- support for only specific modifiers have been implemented
if not ALLOWED_MODIFIERS[modifier] then
return notify.warn("Modifier " .. vim.inspect(modifier) .. " is not in allowed list : " .. table.concat(ALLOWED_MODIFIERS, ","))
end
node = lib.get_last_group_node(node)
if node.name == ".." then
return
end

local default_path = utils_ui.Input_path_editor:new(node.absolute_path, opts)

local input_opts = {
prompt = "Rename to ",
default = default_path:prepare(),
completion = "file",
}

node = lib.get_last_group_node(node)
if node.name == ".." then
vim.ui.input(input_opts, function(new_file_path)
utils.clear_prompt()
if not new_file_path then
return
end

local namelen = node.name:len()
local directory = node.absolute_path:sub(0, namelen * -1 - 1)
local default_path
local prepend = ""
local append = ""
default_path = vim.fn.fnamemodify(node.absolute_path, modifier)
if modifier:sub(0, 2) == ":t" then
prepend = directory
end
if modifier == ":t:r" then
local extension = vim.fn.fnamemodify(node.name, ":e")
append = extension:len() == 0 and "" or "." .. extension
end
if modifier == ":p:h" then
default_path = default_path .. "/"
M.rename_node_to(node, default_path:restore(new_file_path))
if not M.config.filesystem_watchers.enable then
require("nvim-tree.actions.reloaders.reloaders").reload_explorer()
end

local input_opts = {
prompt = "Rename to ",
default = default_path,
completion = "file",
}

vim.ui.input(input_opts, function(new_file_path)
utils.clear_prompt()
if not new_file_path then
return
end

M.rename(node, prepend .. new_file_path .. append)
if not M.config.filesystem_watchers.enable then
require("nvim-tree.actions.reloaders.reloaders").reload_explorer()
end

find_file(utils.path_remove_trailing(new_file_path))
end)
end
find_file(utils.path_remove_trailing(new_file_path))
end)
end -- M.prompt_for_rename

function M.rename_basename(node)
return M.prompt_for_rename(node, { basename = true })
end
function M.rename_absolute(node)
return M.prompt_for_rename(node, { absolute = true })
end
function M.rename(node)
return M.prompt_for_rename(node, { filename = true })
end
function M.rename_sub(node)
return M.prompt_for_rename(node, { dirname = true })
end
function M.rename_relative(node)
return M.prompt_for_rename(node, { relative = true })
end

--- @deprecated
M.fn = function()
-- Warn if used in plugins directly
error("nvim-tree: method is deprecated, use rename_* instead; see nvim-tree.lua/lua/nvim-tree/actions/fs/rename-file.lua", 2)
end

function M.setup(opts)
Expand Down
11 changes: 6 additions & 5 deletions lua/nvim-tree/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,12 @@ Api.tree.winid = wrap(require("nvim-tree.view").winid)
Api.fs.create = wrap_node_or_nil(require("nvim-tree.actions.fs.create-file").fn)
Api.fs.remove = wrap_node(require("nvim-tree.actions.fs.remove-file").fn)
Api.fs.trash = wrap_node(require("nvim-tree.actions.fs.trash").fn)
Api.fs.rename_node = wrap_node(require("nvim-tree.actions.fs.rename-file").fn ":t")
Api.fs.rename = wrap_node(require("nvim-tree.actions.fs.rename-file").fn ":t")
Api.fs.rename_sub = wrap_node(require("nvim-tree.actions.fs.rename-file").fn ":p:h")
Api.fs.rename_basename = wrap_node(require("nvim-tree.actions.fs.rename-file").fn ":t:r")
Api.fs.rename_full = wrap_node(require("nvim-tree.actions.fs.rename-file").fn ":p")
Api.fs.rename_node = wrap_node(require("nvim-tree.actions.fs.rename-file").rename)
Api.fs.rename = wrap_node(require("nvim-tree.actions.fs.rename-file").rename)
Api.fs.rename_sub = wrap_node(require("nvim-tree.actions.fs.rename-file").rename_sub)
Api.fs.rename_basename = wrap_node(require("nvim-tree.actions.fs.rename-file").rename_basename)
Api.fs.rename_relative = wrap_node(require("nvim-tree.actions.fs.rename-file").rename_relative)
Api.fs.rename_full = wrap_node(require("nvim-tree.actions.fs.rename-file").rename_absolute)
Api.fs.cut = wrap_node(require("nvim-tree.actions.fs.copy-paste").cut)
Api.fs.paste = wrap_node(require("nvim-tree.actions.fs.copy-paste").paste)
Api.fs.clear_clipboard = wrap(require("nvim-tree.actions.fs.copy-paste").clear_clipboard)
Expand Down
161 changes: 161 additions & 0 deletions lua/nvim-tree/utils-ui.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
--- Various utility classes and functions for vim.ui.input

local M = {}

--- Options affect what part of the path_base the :prepare() returns
--- At least one field must be specified
--- @class InputPathEditorOpts
--- @field basename boolean|nil - basename of the path_base e.g. foo in foo.lua
--- @field absolute boolean|nil - absolute path: the path_base
--- @field filename boolean|nil - filename of the path_base: foo.lua
--- @field dirname boolean|nil - parent dir of the path_base
--- @field relative boolean|nil - cwd relative path
--- @field is_dir boolean|nil - hint whether path_base is a directory

--- @class InputPathEditorInstance
--- @field constructor InputPathEditor
--- @field opts InputPathEditorOpts
--- @field prepare fun(self):string
--- @field restore fun(self, path_modified: string):string

--- Class to modify parts of the path_base and restore it later.
--- path_base is expected to be absolute
--- The :prepare() method returns a piece of original path_base; it's intended to be modified by user via `vim.ui.input({ default = prepared_path })` prompt.
--- The opts determines what part the path_base :prepare() will return.
--- The :restore(path_modified) to restores absolute :path_base with user applied modifications.
--- Usage example (uncomment, put at the end, and run :luafile %):
--- local Input_path_editor = require("nvim-tree.utils.vim-ui").Input_path_editor
--- local INPUT = vim.fn.expand "%:p"
--- local i = Input_path_editor:new(INPUT, { dirname = true })
--- local prompt = i:prepare()
--- print(prompt)
---
--- vim.ui.input({
--- prompt = "Rename path to: ",
--- default = prompt,
--- }, function(default_modified)
--- default_modified = default_modified and i:restore(default_modified) or i:restore(prompt)
--- vim.cmd "normal! :" -- clear prompt
--- local OUTPUT = default_modified
--- print(OUTPUT)
--- end)
--- @class InputPathEditor
--- @field new fun(self: InputPathEditor, path_base: string, opts?: InputPathEditorOpts): InputPathEditorInstance
--- @field prototype InputPathEditorInstance
--- @diagnostic disable-next-line: missing-fields
M.Input_path_editor = { prototype = { constructor = M.Input_path_editor } }
M.Input_path_editor._mt = {
__index = function(table, key)
if key == "constructor" then
return M.Input_path_editor
end
return table.constructor.prototype[key] or table.constructor.super and table.constructor.super.prototype[key]
end,
}
M.Input_path_editor.fnamemodify = vim.fn.fnamemodify
--- Create new vim.ui.input
--- @param path string path to prepare for prompt
function M.Input_path_editor:new(path, opts)
local instance = {}
instance.constructor = self
setmetatable(instance, self._mt)

local opts_default = { absolute = true }
if opts then
-- at least one opt should be set
local opts_set = false
--- @diagnostic disable-next-line: unused-local
-- luacheck: no unused args
for _, value in pairs(opts) do
if value then
opts_set = true
break
end
end
instance.opts = opts_set and opts or opts_default
else
instance.opts = opts_default
end

local fnamemodify = self.fnamemodify
instance.filename = fnamemodify(path, ":t")
instance.path_is_dir = opts.is_dir or path:sub(-1) == "/"
instance.path_is_dot = instance.filename:sub(1, 1) == "."

if instance.path_is_dir then
path = path:sub(1, #path - 1)
end

-- optimizing
if instance.opts.filename or instance.opts.basename or instance.opts.dirname then
instance.path_dirname = path:sub(1, #path - #instance.filename)
end

if instance.opts.basename then
-- Handle edgy cases where a .dot folder might have .d postfix (.dot.d)
local path_ext = fnamemodify(instance.filename, ":e")
if path_ext == "" then
instance.path_ext = nil
else
instance.path_ext = path_ext
end
end

if instance.opts.relative then
instance.path_relative = fnamemodify(path, ":.")
instance.path_relative_dir = path:sub(0, #path - #instance.path_relative)
end

instance.path = path
return instance
end

--- Extract a piece of path to be modified by ui.input()
--- Put return value into ui.input({ default = <return> })
--- @return string path_prepared
function M.Input_path_editor.prototype:prepare()
local opts = self.opts
local path = self.path
local fnamemodify = self.constructor.fnamemodify
local path_prepared = path

if opts.absolute then
path_prepared = path
elseif opts.filename then
path_prepared = fnamemodify(path, ":t")
elseif opts.basename then
path_prepared = fnamemodify(path, ":t:r")
elseif opts.dirname then
path_prepared = self.path_dirname
elseif opts.relative then
path_prepared = self.path_relative
end

return path_prepared
end

--- Restore prepared path by using path_modified
--- @return string path_modified
function M.Input_path_editor.prototype:restore(path_modified)
if type(self.opts) ~= "table" then
error("you have to call :prepare(...) first", 2)
end

local opts = self.opts
local path_restored = self.path
if opts.absolute then
path_restored = path_modified
elseif opts.filename then
path_restored = self.path_dirname .. path_modified
elseif opts.basename then
path_restored = self.path_dirname .. path_modified .. (self.path_ext and "." .. self.path_ext or "")
elseif opts.dirname then
path_restored = path_modified
elseif opts.relative then
path_restored = self.path_relative_dir .. path_modified
end

return path_restored
end

return M