Skip to main content

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