Keeps files and the types inside them in step.
| You do this | This happens |
|---|---|
Rename Widget.php to Gadget.php |
class Widget becomes class Gadget; references and imports follow |
Move Models/Widget.php to Contracts/ |
The namespace is rewritten; every use statement follows |
Rename class Widget with grn |
The file becomes Gadget.php; references and imports follow |
The scope is renaming and moving. Creating and deleting files are not refactorings, so they are out.
Neovim 0.11 or newer. No dependencies.
The work is done by your language servers, so what you get depends on which you run.
| Language | Rename or move a file | Rename a type |
|---|---|---|
| PHP | phpactor | intelephense 1 |
| TypeScript, JavaScript | vtsls 2 | — 3 |
| Lua | lua_ls | — 3 |
- intelephense's rename is a premium feature.
- Needs
typescript.updateImportsOnFileMove.enabled = "always"in its settings; the default of"prompt"does nothing outside VSCode. - Renaming a type only renames the file for filetypes listed in
filename_is_type, which is PHP by default.
-- lazy.nvim
{
"ncphillips/file-refactor.nvim",
opts = {},
}-- vim.pack
vim.pack.add { "https://github.com/ncphillips/file-refactor.nvim" }
require("file-refactor").setup {}If you use oil.nvim, turn off its own file operations so requests are not sent
twice. setup warns once if you forget.
require("oil").setup {
lsp_file_methods = { enabled = false },
}Defaults shown — setup {} gives you all of this, so pass only what you want to
change.
require("file-refactor").setup {
post_move_servers = { phpactor = true },
filename_is_type = { php = { "Class", "Interface" } },
keymap = "grn",
timeout_ms = 3000,
integrations = { oil = true, nvim_tree = true },
}post_move_servers and filename_is_type are keyed maps, not lists, so your
config extends them rather than replacing them:
require("file-refactor").setup {
post_move_servers = { some_server = true }, -- phpactor stays on
filename_is_type = { java = { "Class", "Enum" } }, -- php stays as it is
}Set an entry to false to turn it off:
require("file-refactor").setup {
post_move_servers = { phpactor = false },
filename_is_type = { php = false },
keymap = false,
}Servers that want workspace/willRenameFiles sent after the move rather than
before it. See Ordering.
Which filetypes get a file rename when you rename a type, and which
SymbolKind values count. Everywhere else, rename() is an ordinary
rename and the filename is left alone.
The distinction is whether the language requires the two to match. PSR-4 makes
it a hard requirement in PHP: a class in a file of another name is not
autoloadable. TypeScript only has a convention, and one that plenty of good code
ignores — registry.ts exporting Registry is idiomatic, and a module may
export several types.
Java and C# have the same hard requirement as PHP. They are not defaults only because they are untested here:
filename_is_type = {
java = { "Class", "Interface", "Enum" },
}intelephense reports PHP traits and enums as Class, so Class and Interface
between them cover all four PHP declarations.
Mapping for rename(), or false to map it yourself.
How long to block on a pre-move willRenameFiles request. The spec expects the
file operation to wait for the reply, so that request is synchronous.
File managers to hook. Ones you do not have installed are skipped.
Two LSP requests handle a file moving:
workspace/willRenameFilesis sent before the move. The server replies with a workspace edit, which is applied first.workspace/didRenameFilesis a notification sent after the move. The server pushes back its own edit if it wants one.
Neovim's client sends neither, and declares every workspace.fileOperations
capability false — so a server that checks before offering its handler never
gets asked (neovim/neovim#32363). This plugin advertises willRename
and didRename, then sends both from whichever file manager performed the move.
Some servers cannot answer willRenameFiles before the move, because they read
the file at its new path to work out the answer. phpactor derives the new class
name by mapping the new path back through composer's PSR-4 roots, so a pre-move
request fails with TextDocumentNotFound.
Servers named in post_move_servers are asked after the move instead. Everyone
else keeps the spec order.
rename() asks the server for a document symbol whose name matches the
filename. If the cursor is on that symbol's declaration, it performs the
server's ordinary rename, writes the files the edit touched — these servers read
from disk, not from your buffers — and then renames the file through the
pipeline above. Otherwise it falls through to vim.lsp.buf.rename(), so mapping
it over the built-in is safe.
A move of a directory is expanded into the files inside it. Server filters are
written against source files, so **/*.php matches no directory and every
namespace beneath it would otherwise go stale.
require("file-refactor").rename() -- what `keymap` is bound to
require("file-refactor").rename_file(old, new) -- rename a path through the pipelineEach integration is a module in lua/file-refactor/integrations/ exposing
setup(augroup) and returning whether the manager was found. Wire up two
points:
local file_ops = require("file-refactor.file_ops")
-- Before the move, with the paths as they are now:
file_ops.before_move(file_ops.moves_for(old, new, old))
-- After it, with the paths as they have become:
file_ops.after_move(file_ops.moves_for(old, new, new))The third argument to moves_for is whichever of the two paths exists when you
call it — filters stat it to tell a file from a directory.
Then add the module name to integrations. mini.files, neo-tree,
snacks.explorer and yazi are all wanted.
tests/run.sh # every scenario
tests/run.sh php/rename_class # one scenario
tests/run.sh ts/ # one languageEach scenario copies a project out of tests/fixtures/, starts real language
servers against it, drives a real file manager, and prints the resulting source.
Roughly 40 seconds per scenario.
Needs phpactor, intelephense and composer for the PHP scenarios and
vtsls for the TypeScript ones, on PATH or in mason's bin directory.
Scenarios that rename a symbol are skipped unless INTELEPHENSE_LICENCE_KEY is
set.
- phpactor does not rewrite docblock types when a file moves, so
/** @var Widget[] */goes stale. Renaming the type instead goes through intelephense, which does update them. - A rename is not undoable as one step. The edits are undoable per buffer; the file move is not.