Awesome
change-function.nvim
What happens when you combine treesitter and lsp together.
Purpose
change-function.nvim allows you to swap function arguments or parameters and have it be updated for all references across a particular project.
Installation
Use your favourite package manager to install change-function.nvim
via Lazy
{
'SleepySwords/change-function.nvim',
dependencies = {
'MunifTanjim/nui.nvim',
'nvim-treesitter/nvim-treesitter',
'nvim-treesitter/nvim-treesitter-textobjects', -- Not required, however provides fallback `textobjects.scm`
}
}
Usage
There are currently different ways to use this plugin. One automatically using
LSP references (this is the default when using the change_function()
function),
the other uses the quickfix list as a source of the functions to change.
Automatically via LSP references.
This is the default version when running change_function()
. It allows for a
quick way to change function arguments, but sacrifices flexibility.
- Run the
require("change-function").change_function_via_lsp_references()
orrequire("change-function").change_function()
command to open up the reorganisation window. - Swap whatever arguments you need using the specified mappings.
- Press enter and confirm
Using the quickfix list.
- Add your references using a command, for example
vim.lsp.references({includeDeclaration = true})
- Modify the quickfix list with whatever workflow you currently use.
- Run the
:lua require("change-function").change_function_via_qf()
command to open up the reorganisation window. - Swap whatever arguments you need using the specified mappings.
- Press enter and confirm
The quickfix list method allows for
- Much more flexibility, as you can use existing quickfix workflows and use
plugins such as
nvim-bqf
,quicker.nvim
orlistish.nvim
to be able to modify the list and include references that you want to change. - Allows you to be able to see what the functions that will be changed before actually running the command.
local change_function = require('change-function')
-- Default options
change_function.setup({
nui = function(node_name)
return {
enter = true,
focusable = true,
border = {
style = "rounded",
text = {
top = "Changing argument of " .. node_name,
}
},
position = "50%",
size = {
width = "40%",
height = "20%",
},
buf_options = {
modifiable = false,
readonly = false,
},
}
end,
queries = {
rust = "function_params",
}
mappings = {
quit = 'q',
quit2 = '<esc>',
move_down = '<S-j>',
move_up = '<S-k>',
confirm = '<enter>',
},
-- Specifies whether or not to use the selected entry as the arguments for the
-- swapping window or the function at the cursor.
quickfix_source = "entry",
})
vim.api.nvim_set_keymap('n', '<leader>crl', '', {
callback = change_function.change_function,
})
vim.api.nvim_set_keymap('n', '<leader>crq', '', {
callback = change_function.change_function_via_qf,
})
Writing queries for different languages
change-function.nvim
uses treesitter to find the location of argument and
function parameters in order to swap them. By default, change-function.nvim
uses the textobjects.scm
queries as a way to find the parameters. However,
sometimes these queries are not suitable, as they do not contain the function
name captures (we might use the highlights.scm to remedy this in the future),
but also incorrectly match arguments. Writing queries to match the argument is
fortunately quite simple.
The queries used are specified in the config for each filetype, typically they
are named function_params.scm
files and are individual to each language. These
files are stored in the runtime queries
directory, so local configurations are
also able to add or override them.
1. Finding the function calls/declarations
You can use the :InspectTree
command to see how treesitter parses the
language. The items we want to find are the all the function declarations and
function calls. This can be done by having the cursor over a function
call/declaration and seeing where it lands on the tree.
Usually, they would have a similar name to function_item
or call_expression
.
2. Matching the function arguments/parameters
We want to capture the individual arguments from the query so their ranges can
be found and swapped. Usually, these arguments are within the
arguments
/parameters
field of the function declaration or call expression.
To capture the individual arguments we want to append a capture called
parameter.inner
(ie: @parameter.inner
), to the end of the query. As some
parsers may use the expressions themselves (like: binary_expression
), a
placeholder value (_)
could be used.
Ensure, you place both a function declaration and a call expression if your language supports it to update references across your project.
3. Matching the function/method names
You may also would want to add captures for identifiers, such as method_name
and function_name
. This allows for change-function.nvim
to detect if the
cursor is currently on top of a method/function name.
The process is similar to matching functions arguments/parameters, but instead
looking and matching for a node that is similar to identifier
, this can be
found by looking at the highlighted node when your cursor is on top of a
function/call name.
Currently, there is no difference between how method_name
and function_name
are handle, but they may be handled differently in the future.
Match from the root of function declaration/argument
NOTE: When matching function arguments or function names, match them from the root of the function declaration/call, as shown below, as to be able to match the identifier as well as the argument.
Putting this together for Rust
; Match function declarations
(function_item
name: (identifier) @function_name
parameters: (parameters
(parameter) @parameter.inner
)
)
(function_signature_item
name: (identifier) @function_name
parameters: (parameters
(parameter) @parameter.inner
)
)
; Match function calls
(call_expression
function: (field_expression
field: (field_identifier) @method_name
)
arguments: (arguments
(_) @parameter.inner
)
)
(call_expression
function: (scoped_identifier
name: (identifier) @function_name
)
arguments: (arguments
(_) @parameter.inner
)
)
(call_expression
function: (identifier) @function_name
arguments: (arguments
(_) @parameter.inner
)
)
If you have written a nice query, please contribute it here :)
Todo
- Make a nicer UI with nui
- Allow for customisability with nui
- Refactor so
function_declaration.scm
andfunction_call.scm
are the same. - Find a better way to find the function declaration/call, rather than recursively calling parent to find the matched node (as this breaks when you try and use this on a value rather than a function).
- Add more queries for different languages
- One day maybe even the ability to add or remove function args (would probably have to be through specifying a seperator and using that to join arguments, as treesitter only provides the argument location)