Customizing vim: formatting comments (part 2)
This article is a follow up of the previous one, with the aim to discuss a few more tips for writing custom vim functions and explain additional features of the vimscript language.
In this article, we continue with the subject of formatting comments. Today’s problem
is to change multiple lines of adjacent //
comments into a single multiline /* ... */
comment.
Example:
// some big comment
// detailing how some
// big function works
We would like to transform the above comment to this:
/* some big comment
* detailing how some
* big function works
*/
Personal preference, but I dislike having the */
at the end of a line.
Like in the previous article, we want to be able to move to the problematic comment with the cursor, and then use a keybinding to automatically perform the correction.
Building the function
In this case, the cursor could be anywhere in the middle of a long comment, so simply using an if statement to check the next and previous lines like in the previous example won’t work. This gives us a chance to take a look at loops in vimscript.
In vimscript, there are two kinds of loops: for and while. Unfortunately vimscript doesn’t allow for C-style for loops, so you will observe that while loops are a lot more common. The most common uses cases for for loops is to loop through arrays (called lists in vimscript lingo)
Let’s take a look at a simple for loop through an array (list)
:let groceries = ["milk", "eggs", "flour"]
:for product in groceries
: echo product
:endfor
Not particularly useful for our use case here, since we won’t even know how many lines long a comment would be, nor would it be practical to turn comments into lists and process them that way.
So, let’s take a look at a simple while loop:
:let i = 1
:let s = 0
:while (i <= 4)
: let s = s + i
: let i = i + 1
:endwhile
:echo s
This basically works as you’d expect in any other programming language. Encasing the condition in parentheses ()
is not necessary, but it does help to make the code a bit readable.
A note about delimiters in regular expressions
As we will be dealing with a lot of //
in this function, we would end up with some nasty looking regular expressions,
since we’d be having to escape the forward slashes. It’s actually possible to use other characters than /
for regex
delimiters, a common choice is ~
. If we do this, we no longer have to escape the forward slashes in the regular expression
itself, since it does not conflict with the delimiter anymore. This actually can be done not only in vim, but also in other places
that use regular expressions, such as sed. For example:
echo "//" | sed 's~/*~@~'
is a completely valid way to use sed and it will print @
as expected.
Outline
If the cursor is not on a line that beings with //, then bail
If it is, then pick up how many lines our change will affect:
using a while loop, keep going "up", and check if the line still has //
if it does, set the start line to this new line
otherwise, if it does not, quit the while loop
using another while loop, keep going "down" and check if the line still has //
if it does, set the end line to this new line,
if it does not, quit the while loop
if the start line and end line are the same line, it means the //
comment is not a multiline comment, so tell the user and bail
So at the start line, we want to replace // with /*
then, for the remainder, we want to replace // with *
but we want to end the last line on a */ no matter what.
We also want to make sure we can deal with odd cases like
two multiline comments being close to each other.
Besides the points noted above, there is not much else to take away from here, so we can jump right into writing up the function.
The function
function ToMultiLineComment()
let start_line = line(".")
let double_slash = '^\s*\/\/'
if (match(getline(start_line), double_slash) == -1)
echohl WarningMsg | echo 'Cursor not in line with a multi-line "//" comment.' | echohl None
return
endif
let end_line = start_line
while (match(getline(start_line - 1), double_slash) >= 0)
let start_line = start_line - 1
endwhile
while (match(getline(end_line + 1), double_slash) >= 0)
let end_line = end_line + 1
endwhile
if (start_line == end_line)
echohl WarningMsg | echo 'This comment is only a single line in length.' | echohl None
return
endif
if (match(getline(end_line), '^\s*\/\/\s*$') >= 0)
execute end_line
let end_line = end_line - 1
endif
execute start_line
execute 's~//~/*~'
if (end_line >= (start_line+1))
execute (start_line + 1) . ',' . end_line . 's~^\(\s*\)//~\1 *~'
execute end_line + 1
endif
endfunction
As with the example of the previous article, this function could be mapped to a keybinding using the
if so desired.