This should work :
function! CommentBox()
" PREPARATIONS
" prefix is the number we hit before <leader>b
let prefix=v:count1
" We define the Firstchar() function that returns the first character
" of the current line.
function! Firstchar()
execute "normal! _"
return matchstr(getline('.'), '\%' . col('.') . 'c.')
endfunction
" We define the Gcc() function that comments an uncommented line.
function! Gcc()
if Firstchar() !=# s:commentchar
execute "normal gcc"
endif
endfunction
" We define the InsertCommentChar() function that inserts a
" comment character manually, because the gcc mapping is unable to
" comment an empty line.
function! InsertCommentChar()
if getline('.') =~ '\v(\s*|^$)'
execute "normal! i\<c-r>a\e"
endif
endfunction
" We store the character under the cursor inside the s:firstchar variable.
let s:firstchar = Firstchar()
" We open a new line.
execute "normal! o"
" We check whether this new line is empty and add the character 'a' in
" this case.
if getline('.') =~ '\v(\s*|^$)'
execute "normal! ia\e"
endif
" We comment this new line.
execute "normal gcc"
" We store the comment character inside the s:commentchar variable.
" For example, if we are in a python file, then s:commentchar = #
let s:commentchar = Firstchar()
" We delete this new line and go back where we were.
execute "normal! ddk"
" CREATION OF A BOX
" If firstchar and commentchar are different, it means that we were not on
" a commented line when we invoked the function. So we create a box.
if s:firstchar !=# s:commentchar
" We call the gcc mapping to comment the lines.
execute "normal ".prefix."gcc"
" We create the upper border and put the mark a on the line.
execute "normal! O\ea \e78i#\ema"
" We create the lower border and put the mark b on the line.
execute "normal! ".prefix."jo\ea \e78i#\emb"
" We put the comment character inside the register a.
call setreg("a", s:commentchar)
" We ask the global command to insert the comment character on every
" empty line inside the box.
'a,'b g/^$/call InsertCommentChar()
" We go on the upper border and comment it if it's not.
execute "normal! 'a"
call Gcc()
" Same thing for lower border.
execute "normal! 'b"
call Gcc()
else
" DESTRUCTION OF A BOX
" If firstchar and commentchar are different, it means we were not on a
" commented line when we invoked the function, thus we want to undo
" a box, not create one.
" First, we delete the borders and put the marks a and b around it.
execute "silent! normal! ?###\rddma/###\rddkmb"
" Then, we uncomment every line that was inside the box.
'a,'b g/^/execute "normal gcc"
endif
endfunction
" We define <leader>b as a shortcut to create/delete a box.
nnoremap <leader>b :<c-u>call CommentBox()<cr>
There may be rough edges. If someone knows how to improve the code, let me know, I'm interested.
Preparations
The first instruction of the CommentBox()
function is :
let prefix=v:count1
The prefix
variable stores the number that precedes the mapping that will be assigned to this function, which will be <leader>b
.
So if you hit 6<leader>b
, prefix
will store 6 and if you hit directly <leader>b
without prefixing it with a count, prefix
will store 1.
Subfunctions
Next we define 3 functions (inside the main one) :
- the
Firstchar()
function that returns the first character of the
current line.
- the
Gcc()
function that comments a line by calling the gcc
mapping but only if the line is empty or contains only whitespace (with this regex \v(\s*|^$)
).
- the
InsertCommentChar()
function that inserts a comment character manually at the beginning of a line, because the gcc
mapping is unable to comment an empty line.
These functions use 2 special commands : execute
and normal
.
The execute
command can take a string or a concatenation of strings (with the dot operator .
), inside the string it interprets the \e
as the escape key, the \r
as the enter key, the \<c-r>
as the Ctrl + r key combination, evaluates the prefix
variable and sends the result to the normal
command.
The normal
command types every key it reads (in this case it receives the key from the execute
command) as if we were in normal mode.
So, whenever inside the script there's something written after normal
, it will be as if we typed a sequence of keystrokes while in normal mode.
For example, the InsertCommentChar()
function will type : i\<c-r>a\e
.
This will cause the switch to insert mode (i
), then the insertion of the a
register, then go back to normal mode by escaping (\e
).
Indeed, to insert the content of a register in insert mode you can hit Ctrl + r followed by the name of the register.
What will be in the a
register ? The comment character.
Variables
Now that we have defined those functions, we need to set two variables s:firstchar
and s:commentchar
.
Why do we prefix them with s:
?
It's a scope (if you know Python, I think it's a sort of namespace).
The s:
scope is local to the script, without it, the variables would be accessible in the main function but not in the subfunctions that we've just defined.
Indeed when a subfunction calls a variable defined in the main one, vim must know where to look for it.
To define s:firstchar
, we simply call the function Firstchar() :
let s:firstchar = Firstchar()
To define s:commentchar
, it's a bit more tricky.
Inspired by your comment (thanks a lot by the way), we're going to open a new line, insert a character in it, comment the line with the gcc
mapping and then grab the first character of the line.
The implementation is the following :
execute "normal! o"
if getline('.') =~ '\v(\s*|^$)'
execute "normal! ia\e"
endif
execute "normal gcc"
let s:commentchar = Firstchar()
execute "normal! ddk"
Note that we must ensure that this new line is empty (hence the regex \v(\s*|^$)
) before inserting the a
character.
Besides at the end, once we've stored the comment character inside s:commentchar
, we delete our new line (dd
) and go back where we were (k
).
Decision making
Now we need to decide if we want to create or destroy a box.
To do so, we're going to compare the first character of the line we're on when invoking the function with the comment character.
If they are different, it means we weren't on a commented line and want to create a box.
If they are the same, it means we were on a commented line (probably inside a box) and want to undo a box instead of creating one.
That's what the main if
condition inside the main function checks :
if s:firstchar !=# s:commentchar
[...]
else
[...]
endif
Creation of a box
We create a box with these 9 instructions :
execute "normal ".prefix."gcc"
execute "normal! O\ea \e78i#\ema"
execute "normal! ".prefix."jo\ea \e78i#\emb"
call setreg("a", charb)
'a,'b g/^$/execute "normal! i\<c-r>a\e"
execute "normal! 'a"
call Gcc()
execute "normal! 'b"
call Gcc()
Suppose we hit 6<leader>b
.
In this scenario, the normal
command will first receive 3 sequences of keystrokes consecutively :
6gcc
O\ea \e78i#\ema
6jo\ea \e78i#\emb
The normal
command will type them as if we were in normal mode.
6gcc
= we comment the 6 next lines (because we prefix our mapping with 6 in our example)
O\ea \e78i#\e
= we open a line above us (O
), escape (\e
), go into insert mode and type 2 spaces (a
), escape again, insert 78 # characters (78i#
), escape again and put the mark a (ma
) on the line.
At this point, we have inserted a line above the original one, that is commented and contains 78 #
characters.
6jo\ea \e78i#\emb
= we go 6 lines downward and do the exact same thing.
Insert 78 #
characters and put the mark b on the line (mb
).
The box is almost done, but there's still one problem that you mentioned in your initial post.
The gcc
mapping doesn't comment an empty line.
To solve this problem, we're going to insert ourselves the comment character on every empty line, and that's what the fourth and fifth instructions do.
The fourth instruction uses the setreg()
built-in function to put the content of the s:commentchar
variable inside the register a
:
call setreg("a", s:commentchar)
And the fifth instruction uses the global command. The syntax of the global command is the following :
range g/pattern/command
On every line inside range
(for example 10,20
means every line between 10 and 20) that matches pattern
, the global command will execute command
.
The range we're going to use is 'a,'b
(which means every line between mark a and mark b), the pattern will be ^$
(empty line) and the command will be call InsertCommentChar()
.
So the fifth instruction we need is :
'a,'b g/^$/call InsertCommentChar()
The last 4 instructions are there to comment the borders if they aren't already (in some cases it happen).
To navigate between the two borders, again we use the a and b marks that we set previously.
That's it, the box should be created.
Destruction of a box
If we hit the mapping <leader>b
on a line that is commented (s:firstchar
= s:commentchar
), we execute these 2 instructions :
execute "silent! normal! ?###\rddma/###\rddkmb"
'a,'b g/^/execute "normal gcc"
Let's say we're still in our example and we've got a box containing 8 lines (6 for the comments and 2 for the borders).
In this case, the first instruction will send to the normal
command the following keystrokes :
?###\rddma/###\rddkmb
?###\r
= we search for the string ### backward
dd
= we delete the first line of the box (the upper border).
ma
= we're now on the first line commented inside the box, we put the mark a on it
/###\r
= we search for the string ### forward.
dd
= we delete the last line of the box (the lower border).
kmb
= we go one line upward (k
) and then we put the mark b on the line.
Once we've put our marks around our block of text, we want to run the gcc
mapping on each of its line to uncomment it.
To do so, we use the global command again :
'a,'b g/^/execute "normal gcc"
This line can be read like this :
for each line between mark a and b, whenever the global command finds a line matching the pattern ^
(which is every line, since every line has a beginning), it must run the command execute "normal gcc"
on it.
The last line of the code maps the CommentBox()
function to <leader>b
:
nnoremap <leader>b :<c-u>call CommentBox()<cr>
Finally, the silent!
commands are used whenever a search is performed, in case there are errors, they won't show up on screen.
And the difference between the normal
commands and the normal!
commands is the recursiveness. Usually I prefer no recursiveness so I use normal!
, because recursiveness means trouble. But sometimes it's necessary.
Here it's necessary when we call the gcc
mapping, because we want vim to interpret it recursively, otherwise it would probably be interpreted as g
and cc
separately or something like that.