- Published on
Perform code actions cross files in NeoVim
In this post, I'll take an example of the project I'm working on, which is the Apache SkyWalking project, but don't worry if you're not familiar with it, we will not dive into the project itself, but rather how to do code refactorings in NeoVim.
In a recent pull request I worked on, what I needed to do was to add a Java annotation @ElasticSearch.EnableDocValues
to some fields cross many Java classes, you can preview the changes here.
Now, I have summarized which fields I need to add the annotation to, which are:
- Fields with another Java annotation
@Column(name = ...)
- I only need to add
@ElasticSearch.EnableDocValues
to the fields whosename
value in@Column(name = ...)
are the following values:
COMPONENT_ID
COMPONENT_IDS
TIMESTAMP
START_TIME
ENTITY_ID
TAG_KEY
LATENCY
START_TIME_SECOND
START_TIME_NANOS
VALUE
TIME_BUCKET
PERCENTAGE
OPERATION_TIME
CREATE_TIME
OPERATION_TIME
DUMP_TIME
SEQUENCE
TRACE_ID
TIMESTAMP_MILLIS
Now here is how I did it.
Populate the quickfix list that need to be modified
The first step is to populate the quickfix list with the files that need to be modified. To do this, I can use the following command to find and add the files to the quickfix list:
:grep "@Column(.*\(COMPONENT_ID\|COMPONENT_IDS\|TIMESTAMP\|START_TIME\|ENTITY_ID\|TAG_KEY\|LATENCY\|START_TIME_SECOND\|START_TIME_NANOS\|VALUE\|TIME_BUCKET\|PERCENTAGE\|OPERATION_TIME\|CREATE_TIME\|OPERATION_TIME\|DUMP_TIME\|SEQUENCE\|TRACE_ID\|TIMESTAMP_MILLIS\)" **/src/**/*.java
After the quickfix list is populated, I can use the following command to get a glimpse of how many files that need to be modified:
:echo len(getqflist())
It shows there are 83 fields that need to be modified. You can also use the following command to open the quickfix window to see the list of files:
:copen
Add the annotation to the fields
Now that I have the list of files that need to be modified, I can use the :norm
command to perform some operations in command mode, i.e. add the annotation above the fields. Here is the command that I used:
:cdo norm! O@ElasticSearch.EnableDocValues | update
- The
:cdo
command is used to execute a command in each file location in the quickfix list. - The
norm!
command is used to execute a normal mode command. In this case, I usedO
to firstly add a new line above the current line and then add the annotation@ElasticSearch.EnableDocValues
. - The
| update
command is used to save the changes to the file.
OK, now that every field needed to be modified has been added with the annotation, here comes the hardest part, before I added the @ElasticSearch.EnableDocValues
annotation, the original file might or might not have the correct import
statement for the annotation class, so after I added the annotation, there might be many compilation errors. So I need to add the missing import
statements to the files, but I don't want to add a duplicate import statement if it already exists in the file.
Add the missing import statements
In order to add the missing import
statements, I can use the Code Actions feature in NeoVim's LSP, let's see how to import the annotation class @ElasticSearch.EnableDocValues
in a single file first:
- Move the cursor to the begginning of the file by pressing
gg
- Go the the next error diagnostic by pressing
]e
- Press
<leader>ca
to show the available code actions
I can see in the popup there is an option Import 'ElasticSearch' (org.apache.skywalking.oap.server.core.storage.annotation.ElasticSearch)
along with other code actions, I can press <Enter>
directly to apply the first code action.
Now, how to perform the import code actions to all locations in the quickfix list? Here is how I did it:
- First, I need to move the cursor to the beginning of each file by pressing
gg
:
:cfdo norm gg
- Now I can move the cursor to the next error diagnostic, and apply the "import" code action by searching the
Import
keyword in the popup and press<Enter>
, I can do this by running multiple Lua scripts in the command line:
:cfdo lua vim.diagnostic.goto_next({ severity = "error" }); vim.lsp.buf.code_action({filter = function(ca) return ca.title:find('Import', 1, true) == 1 end, apply = true})
Let's break down the command:
:cfdo
is used to execute the command in each file in the quickfix list, I usecfdo
in this case instead ofcdo
because I only need to perform the import action in each file only once, even if there are multiple@ElasticSearch.EnableDocValues
annotations in that file, thus avoiding duplicatedimport
statements.lua
is used to execute a Lua script.vim.diagnostic.goto_next({ severity = "error" })
is used to move the cursor to the next error diagnostic.vim.lsp.buf.code_action
is used to show the code actions popup, but I don't want the results to be a popup, I want to filter the single code action I want and directly apply it.filter = function(ca) return ca.title:find('Import', 1, true) == 1 end
is used to filter the code actions whose title starts withImport
, which is what I want.apply = true
is used to apply the first matching code action directly, and thus no popup and interactive selection is needed.
After running the command, I can see there might be some info messages:
No more valid diagnostics to move to
which is because some files might already have the correct imports
because other fields in the same file might be already using the annotation @ElasticSearch.<other-annotation>
. But anyhow I can see the missing import statements are added to the files, and the compilation errors are gone.