1

I am trying to get vim to do block commenting and uncommenting of highlighted lines while keeping the correct indentation level. I am so frustratingly close.

let b:commentChar='//'
autocmd BufReadPost *.[ch]    let b:commentChar='//'
autocmd BufReadPost *.cpp    let b:commentChar='//'
autocmd BufReadPost *.py    let b:commentChar='#'
autocmd BufReadPost *.*sh    let b:commentChar='#'
function! Docomment ()
  "make comments on all the lines we've grabbed
  execute 's/^\s*/&'.escape(b:commentChar, '\/').' /e'
  nohl
endfunction
function! Uncomment ()
  "uncomment on all our lines
  execute 's/\v(^\s*)\V'.escape(b:commentChar, '\/').'\v\s*/\1/e'
  nohl
endfunction
function! Comment ()
  "does the first line begin with a comment?
  let l:line=getpos("'<")[1]
  "if there's a match
  if match(getline(l:line), '^\s*'.b:commentChar)>-1
    echom "calling uncomment"
    call Uncomment()
  else
    echom "calling docomment"
    call Docomment()
  endif
endfunction
vnoremap <silent> <C-r> :call Comment()<cr><cr>

Current behavior on highlighting the following sample lines and calling C-r:

These are sample lines
What will vim do here?

Results in:

// These are sample lines
What will vim do here?

The output of :messages is calling comment, calling uncomment which tells me that vim is first commenting the first line, then going back to look at the first line, seeing that it starts with a comment, and then uncommenting the second line. However, when calling the Uncomment() function, it works correctly:

// If I started with highlighting
// multiple commented lines

This will be correctly changed to:

If I started with highlighting
multiple commented lines

How do I get vim to only look at the first line once, and then stop re-checking it after it's changed the line?

5
  • the way to insert a single quote in a single-quoted (literal string) is to repeat the single quote. what about execute '''<,''>s/^\s*/&'.escape(b:commentChar, '\/').' /e'?
    – Mass
    Commented May 2, 2018 at 16:55
  • @Mass, that causes it to place n+1 comments on all lines, where n is the number of highlighted lines. I think I understand more about what it is doing from using echom statements; I've edited my post. Commented May 2, 2018 at 18:00
  • Possible duplicate of Understanding CTRL-U combination
    – Mass
    Commented May 2, 2018 at 18:19
  • you probably want vnoremap <silent> <C-r> :<c-u>call Comment()<cr>
    – Mass
    Commented May 2, 2018 at 18:19
  • Yes!!!! This is combination with your first suggested change to add in the range to the Docomment() and Uncomment() functions works perfectly. You are my hero =))). I will write my own answer tomorrow so others know this is solved, or you can write something and I will accept it. Commented May 2, 2018 at 18:36

1 Answer 1

0

This answer is here to 1) show the correct code to paste into a .vimrc to get vim 7.4+ to do block commenting/uncommenting while keeping indentation level with 1 shortcut in visual mode and 2) to explain it.

Here is the code:

let b:commentChar='//'
autocmd BufNewFile,BufReadPost *.[ch]    let b:commentChar='//'
autocmd BufNewFile,BufReadPost *.cpp    let b:commentChar='//'
autocmd BufNewFile,BufReadPost *.py    let b:commentChar='#'
autocmd BufNewFile,BufReadPost *.*sh    let b:commentChar='#'
function! Docomment ()
  "make comments on all the lines we've grabbed
  execute '''<,''>s/^\s*/&'.escape(b:commentChar, '\/').' /e'
endfunction
function! Uncomment ()
  "uncomment on all our lines
  execute '''<,''>s/\v(^\s*)'.escape(b:commentChar, '\/').'\v\s*/\1/e'
endfunction
function! Comment ()
  "does the first line begin with a comment?
  let l:line=getpos("'<")[1]
  "if there's a match
  if match(getline(l:line), '^\s*'.b:commentChar)>-1
    call Uncomment()
  else
    call Docomment()
  endif
endfunction
vnoremap <silent> <C-r> :<C-u>call Comment()<cr><cr>

How it works:

  • let b:commentChar='//' : This creates a variable in vim. the b here refers to the scope, which in this case is contained to the buffer, meaning the currently opened file. Your comment characters are strings and need to be wrapped in quotes, the quotes are not part of what will be substituted in when toggling comments.

  • autocmd BufNewFile,BufReadPost *... : Autocommands trigger on different things, in this case, these are triggering when a new file or the read file ends with a certain extension. Once triggered, the execute the following command, which allows us to change the commentChar depending on filetype. There are other ways to do this, but they are more confusing to novices (like me).

  • function! Docomment() : Functions are declared by starting with function and ending with endfunction. Functions must start with a capital. the ! ensures that this function overwrites any previous functions defined as Docomment() with this version of Docomment(). Without the !, I had errors, but that might be because I was defining new functions through the vim command line.

  • execute '''<,''>s/^\s*/&'.escape(b:commentChar, '\/').' /e' : Execute calls a command. In this case, we are executing substitute, which can take a range (by default this is the current line) such as % for the whole buffer or '<,'> for the highlighted section. ^\s* is regex to match the start of a line followed by any amount of whitespace, which is then appended to (due to &). The . here is used for string concatenation, since escape() can't be wrapped in quotes. escape() allows you to escape character in commentChar that matches the arguments (in this case, \ and /) by prepending them with a \. After this, we concatenate again with the end of our substitute string, which has the e flag. This flag lets us fail silently, meaning that if we do not find a match on a given line, we won't yell about it. As a whole, this line lets us put a comment character followed by a space just before the first text, meaning we keep our indentation level.

  • execute '''<,''>s/\v(^\s*)'.escape(b:commentChar, '\/').'\v\s*/\1/e' : This is similar to our last huge long command. Unique to this one, we have \v, which makes sure that we don't have to escape our (), and 1, which refers to the group we made with our (). Basically, we're matching a line that starts with any amount of whitespace and then our comment character followed by any amount of whitespace, and we are only keeping the first set of whitespace. Again, e lets us fail silently if we don't have a comment character on that line.

  • let l:line=getpos("'<")[1] : this sets a variable much like we did with our comment character, but l refers to the local scope (local to this function). getpos() gets the position of, in this case, the start of our highlighting, and the [1] means we only care about the line number, not other things like the column number.

  • if match(getline(l:line), '^\s*'.b:commentChar)>-1 : you know how if works. match() checks if the first thing contains the second thing, so we grab the line that we started our highlighting on, and check if it starts with whitespace followed by our comment character. match() returns the index where this is true, and -1 if no matches were found. Since if evaluates all nonzero numbers to be true, we have to compare our output to see if it's greater than -1. Comparison in vim returns 0 if false and 1 if true, which is what if wants to see to evaluate correctly.

  • vnoremap <silent> <C-r> :<C-u>call Comment()<cr><cr> : vnoremap means map the following command in visual mode, but don't map it recursively (meaning don't change any other commands that might use in other ways). Basically, if you're a vim novice, always use noremap to make sure you don't break things. <silent> means "I don't want your words, just your actions" and tells it not to print anything to the command line. <C-r> is the thing we're mapping, which is ctrl+r in this case (note that you can still use C-r normally for "redo" in normal mode with this mapping). C-u is kinda confusing, but basically it makes sure you don't lose track of your visual highlighting (according to this answer it makes your command start with '<,'> which is what we want). call here just tells vim to execute the function we named, and <cr> refers to hitting the enter button. We have to hit it once to actually call the function (otherwise we've just typed call function() on the command line, and we have to hit it again to get our substitutes to go through all the way (not really sure why, but whatever).

Anyway, hopefully this helps. This will take anything highlighted with v, V, or C-v, check if the first line is commented, if yes, try to uncomment all highlighted lines, and if not, add an extra layer of comment characters to each line. This is my desired behavior; I did not just want it to toggle whether each line in the block was commented or not, so it works perfectly for me after asking multiple questions on the subject.

Your Answer

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

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