5

As a way of organizing my codes, I like to include some titles as comments with specific patterns (I wish I can add ASCII art!). Consider for example, putting the following to mark a specific block of code:

//===================== Solving a big problem =============================//

//--------------------The most important calculation-----------------------//
     ... some code ...
//------------------------End of the calculation---------------------------//

//--------------------I/O operations are on their way ---------------------//
     ... some code ...
//------------------------End of I/O operations----------------------------//
//============================== End solution =============================//

What I require:

  1. The pattern should be 80-column wide (including identation space)
  2. The title should be centered

Is there a way I can automate the generation of such patterns in vim?

1
  • I wrote a little plugin "blockit" (github.com/sk1418/blockit) to generate some header texts (comment could be done as well) with alignments, width, border characters as options , it may not work exactly as what you want, but you can check the codes to do some adjustment.
    – Kent
    Aug 17, 2015 at 11:27

4 Answers 4

8

Please don't do that: This type of title comments is just too heavy and doesn't improve the readability of your code. If you really need to create a section use light multi lines comments like:

"
" Solving a big problem
"

That's much easier to read. So now that I gave my totally subjective opinion, which you don't have to agree with:

Here's a function to let you do what you want

function! MyComment(...)

    " get the arguments properly 
    if (a:0 == 0) 
        let argString = "" 
        let argChar = "-" 
    elseif (a:0 == 1) 
        let argString = a:1 
        let argChar = "-"
    elseif (a:0 == 2) 
        let argString = a:1 
        let argChar = strpart(a:2, 0, 1)
    else 
        echom "Too many arguments" 
        return
    endif

    " Get the characters to use at the beginning and the end of the title
    let result = substitute(&commentstring, "commentstring=", "", "") 
    let resultList = split(result, "%s") 
    let Comment = resultList[0] 
    if (len(resultList)>1) 
        let EndComment = resultList[1] 
    else 
        let EndComment = Comment
    endif

    " create the comment title 
    let nb = (80 - strlen(argString) - len(Comment) - len(EndComment)) / 2 
    let i = 0 
    let commentString = argString

    while i < nb 
        let i = i + 1 
        let commentString = argChar . commentString . argChar
    endwhile

    " append one more char if necessary 
    if strlen(argString) % 2 != 0 
        let commentString = commentString . argChar 
    endif

    let commentString = Comment . commentString . EndComment

    " Put the title in the buffer 
    exe ":normal o" 
    exe ":normal i" . commentString

endfunction

You can call the function in 3 different ways:

  • :call MyComment() will insert an "empty title" of 80 characters like this: //-------------------------------------------------------------------//

  • :call MyComment("Title section") will insert a title with the default - character like this:

    //--------------------------Title section----------------------------//

  • :call MyComment("Another title", ".") will let you choose the character to repeate in your title (If the second string contains more than one character, only the first one will be used):

    //...........................Another title............................//

Also with this function if you are editing a C file, using :call MyComment("Hello world") will insert:

/*--------------------------------Hello world---------------------------------*/

In a java file you'll get:

//--------------------------------Hello world---------------------------------//

And a php file:

/*--------------------------------Hello world---------------------------------*/

Improvements

  • For now the function doesn't takes into consideration the indentation level, if I get motivation I'll edit my answer otherwise I'll let it as an exercise for the reader ;-) (remember that you can get the indent level of a line with :foldlevel(x) this page might help).

  • It would also be possible the create a command to shorten the call of the function, this tricky part might be to handle the optional number of arguments.


EDIT In the comments @Peter Rincker suggested a much more elegant solution than mine. With his authorization I'll present here a new version inspired by his function and the explanation of the different tricks he used:

function! MyComment(...)

    " get the arguments properly
    let commentText = get(a:000, 0, '')
    let commentChar = get(a:000, 1, '-')[0]

    " Get the number of char to add on left
    let len   = (80 - len(commentText) - len(printf(&commentstring, '')))
    let left  = len / 2
    let right = len - left

    " force the title with repeat() function and insert it in the buffer
    put=printf(&commentstring, repeat(commentChar, left) . commentText . repeat(commentChar, right))

endfunction

First the part where he gets the arguments:

    let commentText = get(a:000, 0, '')

According to :h function-argument a:000 is a dictionary containing all the arguments, then a:000[0] is equivalent to a:1

The function get() third parameter is a default value. This syntax allows to greatly reduce the ugly if...else structure that I originally used.

Another cool trick is to use printf(&commentstring, '') to replace the %s part of the string. Once again much cleaner than my substitute workaround

And finally the reason I used his function: the use of repeat():

printf(&commentstring, repeat(commentChar, left) . commentText . repeat(commentChar, right))

The function repeat the expression passed as first parameter, the number of time passed as second parameter and return the concatenated result. This way in a single line two loops are written in a very readible format.

Many thanks to Peter for the improved version!

7
  • 2
    You could even use the utf-8 linedrawing characters: '╔═╗║╚═╝┌─┐│└─┘', If you want to create 'configuration'-files, that can also double as 'documentation', and will look like something to the boss, when he sees it printed on your wall. :) Aug 16, 2015 at 11:33
  • May want to use repeat() instead of a loop. See :h repeat() Aug 17, 2015 at 7:01
  • @PeterRincker nice function I didn't knew it. What is its advantage compared to a loop (excepted the fact that it requires less lines to write it)?
    – statox
    Aug 17, 2015 at 8:04
  • @statox there are 2 reasons to use repeat over a loop. 1) shorter 2) quick comprehension - instead of having to read and understand the loop, you can easily understand what is going on. As an example compare your function to mine. Aug 17, 2015 at 11:11
  • @PeterRincker Indeed that's a great way to rewrite it! I'll investigate the repeat command which seems really good, thanks.
    – statox
    Aug 17, 2015 at 12:08
5

This is a perfect use case for ultisnips with Python interpolation. A sketch of a snippet for the first header might look like this:

global !p
from math import floor, ceil

def left(lng, fill, s):
    count = int(floor((lng - len(s) - 6) / 2.0))
    if count < 0: count = 0
    return fill * count

def right(lng, fill, s):
    count = int(ceil((lng - len(s) - 6) / 2.0))
    if count < 0: count = 0
    return fill * count
endglobal

snippet com "Centered comments" b
//`!p snip.rv = left(80, '=', t[1])` ${1:Solving a big problem} `!p snip.rv = right(80, '=', t[1])`//
$0
endsnippet
3
  • I don't use utlisnipts but to sounds like a great idea to use it: that might be a good way not to reinvent the wheel!
    – statox
    Aug 16, 2015 at 17:05
  • Since you're using Python anyway, try string formatting like in my post: vi.stackexchange.com/a/585/205
    – muru
    Sep 24, 2015 at 0:13
  • @muru Well I'm afraid I have no idea how to do that. The naive attempt //`!p snip.rv = '{:=^76}'.format(' ' + t[1] + ' ')`// doesn't work, and neither does //`!p snip.rv = '{:=^76}'.format(' ' + t[1] + ' ')`${1:Solving a big problem}//. My knowledge of ultisnips is not up to the task, sorry. Sep 24, 2015 at 14:03
4

Just don't

I agree with @statox, "just don't do this". However if you do then @Sato Katsura is also correct that the best way is probably to use some snippets.

However if you do not want to use snippets here are some native Vim methods.

'virtualedit'

You can :set virutaledit=all to make it so you can insert text anywhere without worrying about pesky line lengths and spaces. This is great for ASCII art or making comment boxes (use r). Use | to jump to a specific column, e.g. 80| to jump to column 80.

For more help see:

Overwrite with R

Use the R command to replace/overwrite characters. This is a great way to put in your text without having to remove and tinker with your new comment line's length.

For more help see:

Use :center

The :center command will center text (by default to 80 chars). This isn't directly useful however you can use it to get your positioning without doing any hard work. Just create a line with your text then execute :center. Now that you know the starting column you can use R on your actual comment line. Bonus points for using <c-r> to paste the text.

For more help see:

Re-centering

Re-centering can be accomplished using :center, r, R, and <c-r>. (All the r's!)

Starting on the start of the word, do the following:

v/-\+/b-<cr>ygvr-O<c-r>0<esc>:center<cr>jR<c-r>0<esc>kdd

Probably best to make this a command or mapping. Note it has quite a few side-effects.

For more help see:

Re-centering with a substitution or "how I learned to abuse regex backtracking"

Another approach to re-centering is to use a substitution, :s, with a sub-replace-expression.

:s/\v(-+)(.{-})(-+)/\=substitute(submatch(1).submatch(3),'\v(-*)(\1-=)','\1'.escape(submatch(2),'&~').'\2','')/

Overview:

Capture the text and the leading and trailing characters, - in this case. We use a sub-replace-expression to do another substitution to divide up the dashes in half. This balancing act is done via the regex engine's backtracking because math is hard (let's go shopping).

Notes:

Probably best to make this into a command:

:command! -range -bar CenterComment execute <line1>.','.<line2>.'s/\V\^'.printf(escape(&commentstring, '\/'), '\zs\(\.\=\)\(\1\*\)\(\.\{-}\)\(\1\*\)\ze')."\\$/\\=substitute(submatch(1).submatch(2).submatch(4),'\\v(.*)(\\1.=)','\\1'.escape(submatch(3),'&~').'\\2','')/"

Now you can just use :CenterComment on the comment line to recenter text.

For more help see:

Thoughts

Once again, just don't worry about this. There are plenty other things to worry about, e.g. your code and quality of documentation.

Personally if I was going to use these kind of comments I would use some kind of snippet and :CenterComment command.

2

Here's a shorter implementation, interactive (will ask for the comment), inspired from the old-and-true IBM "Vim Scripting" guide (part 2, listings 13 & 14) and adapted for the OP request:

" ========= fancy comments ====================================================
" inspired from http://www.ibm.com/developerworks/library/l-vim-script-2/
" (listings 13 & 14)
function! CommentBlock(comment, intro, end, box_char, ...)
    " linewdith is 1ist computed from the 'textwidth' if set, 80 if not
    let linewidth = &textwidth> 0? &textwidth : 80
    " but can be overwritten by the optional argument value:
    let linewidth = a:0 >= 1 ? a:1 : linewidth

    let comwidth = strlen(a:comment) + 2
    let boxwidth1 = (linewidth - comwidth - len(a:intro) - len(a:end)) / 2
    let boxwidth1 = boxwidth1>0 ? boxwidth1 : 0

    let boxwidth2 = (linewidth - len(a:intro) -len(a:end) - boxwidth1 - comwidth)
    return a:intro . repeat(a:box_char, boxwidth1) . ' ' . a:comment
                \ . ' ' .repeat(a:box_char, boxwidth2) . a:end
endfunction

No indentation support, though.

Mapping example:

inoremap <silent> //* <C-R>=CommentBlock(input("Comment> "), '//', '//', '*')<cr>
inoremap <silent> //= <C-R>=CommentBlock(input("Comment> "), '//', '//', '=')<cr>
inoremap <silent> //- <C-R>=CommentBlock(input("Comment> "), '//', '//', '-')<cr>
inoremap <silent> //x
            \ <C-R>=CommentBlock(input("Comment (40)> "), '/*', '*/',  'x', 40)<cr>

Results:

//******************************* hello world ********************************//
//============================ this is a comment? ============================//
//------------------------------ minus for you -------------------------------//
/*xxxxxxxxxxx asdfasdfasdf xxxxxxxxxxx*/    

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.