Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

file-refactor.nvim

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.

Requirements

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
  1. intelephense's rename is a premium feature.
  2. Needs typescript.updateImportsOnFileMove.enabled = "always" in its settings; the default of "prompt" does nothing outside VSCode.
  3. Renaming a type only renames the file for filetypes listed in filename_is_type, which is PHP by default.

Install

-- 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 },
}

Configuration

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,
}

post_move_servers

Servers that want workspace/willRenameFiles sent after the move rather than before it. See Ordering.

filename_is_type

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.

keymap

Mapping for rename(), or false to map it yourself.

timeout_ms

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.

integrations

File managers to hook. Ones you do not have installed are skipped.

How it works

Two LSP requests handle a file moving:

  • workspace/willRenameFiles is sent before the move. The server replies with a workspace edit, which is applied first.
  • workspace/didRenameFiles is 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.

Ordering

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.

Renaming a type

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.

Directories

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.

API

require("file-refactor").rename()              -- what `keymap` is bound to
require("file-refactor").rename_file(old, new) -- rename a path through the pipeline

Adding a file manager

Each 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

tests/run.sh                    # every scenario
tests/run.sh php/rename_class   # one scenario
tests/run.sh ts/                # one language

Each 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.

Limitations

  • 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.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages