Nvim: Updating Header Comments
The alignment of documentation and code might be challenging. You either have
to always keep in mind to update the relevant comments or they get deprecated
really fast. Fortunately, using a powerful editor like nvim
can help us
with this challenge.
Motivation
Every of my source files includes a header comment containing the most relevant information about the module such as a brief description of its purpose, the last time the file has been touched and the version this file belongs to. It also includes me as the author in case someone needs to clarify implementation details. While one can enter the information manually upon creation, it is prone to get deprecated very fast.
Let us take a look at a more concrete example containing the most relevant fields.
/********************************************************//** * \file matrix_live_patch.rs * * \brief Algorithm to patch the matrix in production. * * \details The glitch causing the deja vu effect for * underlying elements is to be investigated. * * \author [MS] Michael Schupikov * * \created 14.12.2019 * * \date 14.12.2019 * * \version 1.4.12 [+] * ***********************************************************/
Sure, if we ever fix this matrix glitch we would need to update the documentation. Additionally, the creation date needs to be valid. Apart from that, what fields should be updated on a regular basis? The date is the most obvious. It is more than easy to forget while introducing changes.
Next one is the version. It might look easy at first because all you have to do is to jump there and change it. However, what if you have 50 source files in your project? Do you modify each file individually? Do you modify the version only for those files that have been changed since the last one? Also, what happens if you have modified the file but it is not time yet to announce the next version? Does your comment always contains your last version despite changes?
Finally, what about the file? It might look static at first, but what if you rename it to something else? Do you remember to update the header comment then?
The bottom line is that it is difficult to keep the header comment aligned
with the state of your file. Luckily, we are able to use nvim
to our
advantage.
What do I want?
On file creation, I want the creation date to be set automatically. Moreover,
I want nvim
to update the date as soon as I make any modification to the
code. I want to see right away when the last change has been made and trust
that information.
Additionally, I would like the version to automatically indicate whether it is a clean version or if some changes have been introduced. Ideally, I would like to have a counter for the updates on the file.
Lastly, I want the file in the comment to match the actual file in the file system even if I rename it.
Implementation
With nvim
and its vim script I can meet my expectations. I am using the
built-in script here as I want the implementation to be as portable as
possible. Let us start with the basic function for substitution first.
function s:substitute_header(pat, rep) let l:start = 1 let l:end = 20 let l:lineno = l:start while l:lineno <= l:end let curline = getline(l:lineno) if (match(curline, a:pat) != -1) let newline = substitute(curline, a:pat, a:rep, '') if (newline != curline) keepjumps call setline(l:lineno, newline) endif endif let l:lineno = l:lineno + 1 endwhile endfunction
In substitute_header()
we expect the pattern to be replaced and what it
should be replaced with in pat
and rep
, respectively. The part of the
identifier before the colon indicates its scope. For example,
s:substitute_header()
refers to function substitute_header()
in the
script. Arguments are denoted by a
and local variables by l
.
Firstly, we iterate over the lines in the given range. We expect the header
information between lines 1 and 20. For the matching line we call the built-in
function substitute()
and set the line with setline()
. With
keepjumps
we make sure that the jump marks are not affected by the
substitution. This is the most difficult part of our implementation. With
substitute_header()
out of the way, we can proceed to the more interesting
parts.
autocmd BufWritePre * call s:update_timestamp() function s:update_timestamp() let l:pat = '\(\([Ll]ast\)\?\s*\(\\[Dd]ate\?\)\s*\).*' let l:rep = '\1' . strftime("%d.%m.%Y") call s:substitute_header(l:pat, l:rep) endfunction
In update_timestamp()
we set the date field to match the current day. As
search pattern we use everything similar to “date”. Consult vim
s reference for “pattern” if you are not familiar with atomic groups in
particular or regular expressions in general. The implementation is otherwise
straightforward by using substitute_header()
.
The date is supposed to be updated before we are writing the buffer, so add
the corresponding call to the BufWritePre
event 1.
autocmd BufNewFile * call s:update_filename() autocmd BufReadPost * call s:update_filename() function s:update_filename() let l:pat = '\(\\[Ff]ile\?\s*\).*' let l:rep = '\1' . expand('%:t') call s:substitute_header(l:pat, l:rep) endfunction
The theme for the next couple of functions will stay the same. We specify what
we are looking for and what we would like to have instead. In
update_filename()
, we replace the filename with the current one and
trigger the function on creating or reading a file. The former is used in
conjunction with templates.
autocmd BufWritePre * call s:update_version_dirty() function s:update_version_dirty() let l:pat_key = '\(\([Ll]ast\)\?\s*\(\\[Vv]ersion\?\)\s*\)' let l:pat_val = '\(\S\+\)\(\s*\[+]\)\?' let l:pat = l:pat_key . l:pat_val let l:rep = '\1\4 [+]' if (&mod) call s:substitute_header(l:pat, l:rep) endif endfunction
Similar to update_filename()
, update_version_dirty()
matches anything
remotely looking like “version” and replaces it with the version along with
the suffix “ [+]” to indicate that the file has been modified since the last
clean version. Like update_timestamp()
it is triggered before we are
writing the buffer to the file system. The regex is qute complicated, but all
it does is to select the different parts of the version information. Then, we
are able to use separate subgroups for the result.
Until now we have implemented all the functionality that is needed in order to update the fields of interest automatically. However, functionality to deal with the version in a better way is desirable.
command -nargs=1 Version call s:update_version_clean(<args>) function s:update_version_clean(version) let l:pat = '\(\([Ll]ast\)\?\s*\(\\[Vv]ersion\?\)\s*\).*' let l:rep = '\1' . a:version call s:substitute_header(l:pat, l:rep) noa update endfunction
The function to set a clean version is not different from the previous
implementations. Note that we do not trigger update_version_clean()
through an event since we want to set a clean version explicitly. Instead, we
are providing a command to update the version to the given one. This function
does not apply the suffix “ [+]“ to it. Also, note that we are disabling any
auto-commands for update
as we especially do not want
update_version_dirty()
to interfere with the update.
If you have really many files and want to update them, you do not want to perform the step separately for each file. To avoid that, we additionally define a command to update the versions in all open files.
command -nargs=1 Versions call s:update_versions(<args>) function s:update_versions(version) let l:currBuff=bufnr("%") bufdo! call s:update_version_clean(a:version) execute 'buffer ' . l:currBuff endfunction
We store the current buffer, update the version for each buffer and return to
the original one. Just as with update_version_clean()
, we define a command
for the function. Note that updating the version in nvim
instead of using
a stand-alone script enables you to still go back in history, as nvim
is
able to track the changes.
Now, the header comments are updated as we would like and the commands to set the version explicitly are also available. Additionally, I define mappings for those commands for facilitated access.
noremap v :Version ''<left> noremap <m-v> :Versions ''<left>
Note that in my current configuration the key ‹v›
is completely unmapped,
so I am using that for version-related duties. Key ‹v›
sets the version in
the current file and ‹alt-v›
sets the versions in all open files.
Note
Only existing fields are updated using this approach. Therefore, your snippet for file header comments should contain those for them to be updated.
Templates
Until now the header is updated for existing files. This is nice. However,
nvim
has also this nice feature of templates. Templates are file
skeletons that are read into a newly created file based on its extension, for
example. Those might also contain a header comment and we would like to update
it directly on file creation. This is exactly what load_template()
is for.
autocmd BufNewFile *.* silent! call s:load_template() function s:load_template() let l:template_dir = '~/.config/nvim/templates/' let l:extension = expand("<afile>:e") execute '0r ' . l:template_dir . 'template.' . l:extension call s:update_timestamp_creation() call s:update_timestamp() endfunction function s:update_timestamp_creation() let l:pat = '\(\(\\[Cc]reated\?\)\s*\).*' let l:rep = '\1' . strftime("%d.%m.%Y") call s:substitute_header(l:pat, l:rep) endfunction
In load_template()
we specify the path where all the templates live. In
it, the file to load is template.<extension>
, where <extension>
is the
extension of your new file. If a template is found, its contents are used as
the template for the new file. We already update the file name for any file
that is opened or created. The only thing we have to do in load_template()
is taking care of the current timestamp and the date of creation.
Note
Only existing fields are updated using this approach. Therefore, your template should contain those for them to be updated.
Conclusion
I have shown how I update some of the fields in the header comment
automatically in nvim
. With that knowledge equipped, we at least can trust
the correct filename, creation date, date of the last modification and whether
changes have been made since the last version.
It is one thing to test the changes with the purpose of developing a solid
functionality in nvim
. However, nothing is better than being surprised by
the automatic updates later on, while actually working on code.
- 1
-
Note that we could have used event groups here for all header realated events, but I am omitting those for brevity.