Customizing vim: formatting comments (part 1)
Though it’s easy enough to grasp the basics of vim, being able to script your own functionalities into the program is one of its greatest assets and what makes it so incredibly extensible. This will be a series on going through a few examples to see how a little bit of work can end up saving a lot of time down the line, while also explaining some of the features of the vimscript language.
The reader is assumed to be able to already use vim in interactive mode, and be familiar with all the basic keybindings and modes of vim. Furthermore, the reader is assumed to be familiar with basic regular expressions.
The aim of this short article is to explain how to create a function using
vimscript to clean up the formatting of comments that are single or double line
/* */
comments and replace them with //
.
Example:
/* some minor comment
*/
We want to transform the above comment to this:
// some minor comment
We also want to deal with the case where a single line of comment looks like this:
/* some minor comment */
Delving into this will also allow us to take a look at a few of vim’s useful built in commands.
Ideally, we want something simple, e.g. move to the problematic line with the cursor, and then use a keybinding to automatically perform the correction. So let’s start building the script.
Building the function
To get the line on which our cursor rests, we can use the line()
built in function.
If supplied with .
as the argument, it’ll return the current line number the cursor is on
(or 1 for an empty file).
We also know that we need to match a /*
and a */
at some point, as well as
both at the same time, so we can prepare the regular expressions for those as well,
to have them at hand when needed.
That means, so far we got the following:
let start_line = line(".")
let start_pat = '^\s*\/\*'
let end_pat = '^\s*\*\/\s*$'
let single_pat = '^\s*\/\*\([^*\\]\|\\.\)*\*\/\s*$'
So now we need to be able to check whether a particular string matches with a pattern or not.
For this, we can use the built in match()
function, which requires two inputs.
A string, and a pattern to match. If match()
finds the requested pattern, it’ll return the
position in the line where the match was found. If the requested pattern was not found, then
it returns -1.
This means that just having the line number isn’t quite enough, we need the actual content of
the line. This isn’t a problem, as vim has the built in getline()
function for this.
getline()
simply returns the content of the given line of input. It can also accept ranges, so
getline(20,30)
would return the contents of lines 20 to 30 of the currently open file.
Let’s consider a small example file:
1234
asdf
qwer
aaaa
bbbb
Trying out the commands:
:echo match(getline(2), 'f') " returns 3"
:echo match(getline(2), 'x') " returns -1"
That is essentially it. Now we need to simply build a few if statements to consider all the cases. The logic is the following:
Outline
If the line under the cursor starts with '/* and ends on '*/' then
just do the replacement and quit the function.
If the line under the cursor starts with '/*' then
assume the next line will start with '*/'
and assume the next line to be where we will search for '/*'
otherwise
If the line under the cursor starts with '*/' then
assume the previous line will start with '/*'
and assume the previous line to be where we will search for '*/'
If neither of the above cases work out, bail & tell the user.
If we didn't bail as a result of the above condition, do the replacement.
This brings us to a few final points before writing up the full function.
Customizing echo highlights
To print some message back to the user, a simple :echo 'some text'
is of course sufficient,
But ideally, we should print something more like an error message instead of plain white text.
For this, there is a built in function to change the highlighting colour of text to be printed
out to the user.
Example:
:echohl WarningMsg | echo 'This is now in red!' | echohl None
echohl
simply changes the colour of the next echo statement based on the given highlighting group.
‘None’ is the default option. We don’t want vim to keep printing everything in red, so we can set it back to the default by a second application of echohl.
WarningMsg is a built in highlight group, but user defined ones are also easily possible to make:
:hi MyColor ctermfg=200
:echohl MyColor | echo 'This is now in pink!' | echohl None
Because these colours are represented by numbers, they are not too intuitive, so you can check out the vim wiki for reference.
Executing strings as commands
Usually not recommended in most languages and situations. Vim has an execute
command, which
effectively acts as the eval()
of other languages. In vim the concern with this approach is
not as high given the fact that generally, the only source of any input is the user.
Situations where untrustworthy input from external sources are not common when it comes to vim.
Sometimes this command is abbreviated to exe
or exec
but those are also exactly the same as execute
.
Using execute
will allow us to simply apply a substitute regex on the file.
The function
function! FixOneLineComment()
let start_line = line(".")
let start_pat = '^\s*\/\*'
let end_pat = '^\s*\*\/\s*$'
let single_pat = '^\s*\/\*\([^*\\]\|\\.\)*\*\/\s*$'
if match(getline(start_line), single_pat) >= 0
execute 's~^\(\s*\)/\*\(.\{-}\)\s*\*/~\1//\2~'
return
endif
if match(getline(start_line), start_pat) >= 0
let end_line = start_line + 1
elseif match(getline(start_line), end_pat) >= 0
let end_line = start_line
let start_line = end_line - 1
endif
if match(getline(start_line), start_pat) == -1 || match(getline(end_line), end_pat) == -1
echohl WarningMsg | echo 'Invalid operation, cursor should be in a one line /* ... */ comment' | echohl None
return
endif
execute end_line
normal! ddk
execute 's~^\(\s*\)/\*\(.\{-}\)\s*$~\1//\2~'
endfunction
As with any function, it’s possible to avoid having to type out the full function name at every call. That is, instead of
:call FixOneLineComment()
We can map this to a custom key bind, e.g.
nnoremap <leader>y :call FixOneLineComment()<cr>
If you are not familiar with what
is, then, basically, it allows us to map custom
sequences of keys. The leader itself can be chosen by the user, and it needs to be a key that
is not yet mapped to anything. A common choice is for this is ,
.
This means that if we set the
to ,
, then pressing ,
will basically activate our ability
to activate custom key mappings, where the next keypress after ,
will execute whatever function is
associated to that key. Setting the leader to ,
is done by adding the following to beginning
of your .vimrc file:
let mapleader=","
So in our case, pressing y
after ,
will execute this function we just created in this article.
And that bring us to the end of this one.