Skip to content

Commit

Permalink
refactor(actions): common code for rename-file
Browse files Browse the repository at this point in the history
Add `Input_path_editor` new class for common code
Add `api.fs.rename_relative()` based on `Input_path_editor` new class
Hard deprecate private `fn()` in case used directly elsewhere
Replace `fn()` by `rename_*()` based on `Input_path_editor` new class
  • Loading branch information
hinell committed Nov 27, 2023
1 parent d5cc938 commit 04934f6
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 69 deletions.
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

0 comments on commit 04934f6

Please sign in to comment.