I was looking for the same thing. I did a quick a dirty solution for my python project (hook on nvim-tree). I will probably do a similar solution for typescript projects, but since imports are relative it’s gonna be a bit more complicated.
-- This variable must be enabled for tree colors to be applied properly
vim.cmd("set termguicolors")
require 'nvim-tree'.setup {
disable_netrw = true,
hijack_netrw = true,
open_on_setup = false,
ignore_ft_on_setup = {},
open_on_tab = false,
hijack_cursor = false,
update_cwd = true,
-- update the focused file on `BufEnter`, un-collapses the folders recursively until it finds the file
update_focused_file = { enable = false, update_cwd = false, ignore_list = {} },
system_open = { cmd = nil, args = {} },
view = { width = 40, side = 'left', mappings = { custom_only = false, list = { { key = "e", action = "cd" } } } },
actions = { open_file = { resize_window = false } },
renderer = { highlight_opened_files = "all" }
}
local api = require('nvim-tree.api')
local Event = require('nvim-tree.api').events.Event
local function is_existing_dir(path)
local f = io.open(path, "r")
if not f then return false end
local ok, err, code = f:read(1)
f:close()
return err == "Is a directory"
end
local function is_existing_file(path)
local f = io.open(path, "r")
if f ~= nil then
io.close(f)
return true
else
return false
end
end
local function log_in_file(text)
local out_file = io.open("~/Desktop/vimproto/log.txt", "a")
io.output(out_file)
io.write(text)
io.close(out_file)
end
local function on_node_renamed(data)
local function escaped_replace(str, what, with)
what = string.gsub(what, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1") -- escape pattern
with = string.gsub(with, "[%%]", "%%%%") -- escape replacement
return string.gsub(str, what, with)
end
local function ends_with(str, ending)
return ending == "" or str:sub(- #ending) == ending
end
local function is_python_file(path)
return ends_with(path, ".py")
end
local function refactor_file_imports(old_name, new_name)
local function path_to_import_string(path)
local working_dir = vim.fn.getcwd()
local import_string = escaped_replace(path, working_dir, "")
import_string = escaped_replace(import_string, "/", "."):sub(2)
return string.gsub(import_string, "%.py$", "")
end
local old_import_string = path_to_import_string(old_name)
local new_import_string = path_to_import_string(new_name)
local working_dir = vim.fn.getcwd()
local find_and_replace_command =
"find " .. working_dir .. " -name '*.py' -type f -exec sed -i -e 's/" .. "\\<" .. old_import_string .. "\\>" ..
"/" .. new_import_string .. "/g' -- {} +"
local handle = io.popen(find_and_replace_command)
if handle then handle:close() end
end
-- TODO could ignore __pycache__ and .venv
local function refactor_dir_imports(old_dir_path, new_dir_path)
local handle = io.popen("find " .. new_dir_path .. " -type f -name '*.py'")
local python_filepaths = handle:read("*a")
if python_filepaths then
for filepath in python_filepaths:gmatch("([^\n]*)\n?") do
local old_filepath = escaped_replace(filepath, new_dir_path, old_dir_path)
log_in_file(old_filepath .. " --> " .. filepath .. "\n")
refactor_file_imports(old_filepath, filepath)
end
end
if handle then handle:close() end
end
if is_python_file(data.old_name) and is_python_file(data.new_name) then
refactor_file_imports(data.old_name, data.new_name)
end
if not is_existing_dir(data.old_name) and is_existing_dir(data.new_name) then
refactor_dir_imports(data.old_name, data.new_name)
end
vim.api.nvim_command("LspRestart")
end
local function on_dir_created(data)
local function create_empty_file_handle(file_handle_path)
local file_handle = io.open(file_handle_path, "w")
if file_handle ~= nil then
file_handle:write("")
file_handle:close()
end
end
local parent_dir = string.gsub(data.folder_name, "/[^/]+/$", "")
local parent_init = parent_dir .. "/__init__.py"
if is_existing_file(parent_init) then create_empty_file_handle(data.folder_name .. "__init__.py") end
end
api.events.subscribe(Event.NodeRenamed, on_node_renamed)
api.events.subscribe(Event.FolderCreated, on_dir_created)