LSP File Rename

Hi there!
I’m looking for a way to do file renames, as in change the name of a file and have the LSP change all imports and mentions within the project.

Either for the current file or given a file name would work (possibly both).

I’ve been messing around with vim.lsp.buf.rename but haven’t really had any luck :frowning:

Any idea how to approach this? Even a vague indication on where to look would be helpful ^^

Thanks in advance!

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)
2 Likes