First, I'd like to thank @mike for his answer, as I'm using a modified version of it. I wanted to post my version, in case anyone is interested. The main functional difference for mine is that it will always perform the same action on every line of the range. If the selected range contains any uncommented lines, then every line has a comment leader added to it. This way, if you have human-readable comments in your otherwise uncommented code block, they don't become uncommented. Then, when you uncomment the block, the human-readable text will remain commented, since it has 2 comment leaders. It also restores the cursor position when done.
The ToggleComment function:
function! ToggleComment() range
"Ensure we know the comment leader.
if !exists('b:comment_leader')
echo "Unknown comment leader."
return
endif
"Save the initial cursor position, to restore later.
let l:inipos = getpos('.')
"Make a list of all of the line numbers in the range which are already commented.
let l:commented_lines = []
for i in range(a:firstline, a:lastline)
if getline(i) =~ '^\s*' . b:comment_leader
let l:commented_lines = add(l:commented_lines, i)
endif
endfor
"If every line in the range is commented, set the action to uncomment.
" Otherwise, set it to comment.
let l:i1 = index(l:commented_lines, a:firstline)
let l:i2 = index(l:commented_lines, a:lastline)
if l:i1 >= 0 && l:i2 >= 0 && (l:i2 - l:i1) == (a:lastline - a:firstline)
let l:action = "uncomment"
else
let l:action = "comment"
endif
"Loop through the range, commenting or uncommenting based on l:action.
for i in range(a:firstline, a:lastline)
"Move to line i.
exec "exe " . i
"Perform the action.
if l:action == "comment"
exec 'normal! 0i' . b:comment_leader
else
execute 'silent s,' . b:comment_leader . ',,'
endif
endfor
"Restore the initial position.
call setpos('.', l:inipos)
endfunction
The key mapping (note that I changed the comment key to 'k'):
noremap <Leader>k :call ToggleComment()<CR>
Lastly, I put the autocmds inside the "if" statement and augroup below, but they are unchanged. They are mostly just there for reference.
if has("autocmd")
augroup autocmds
autocmd!
autocmd FileType c,cpp,java let b:comment_leader = '//'
autocmd FileType arduino let b:comment_leader = '//'
autocmd FileType sh,ruby,python let b:comment_leader = '#'
autocmd FileType zsh let b:comment_leader = '#'
autocmd FileType conf,fstab let b:comment_leader = '#'
autocmd FileType matlab,tex let b:comment_leader = '%'
autocmd FileType vim let b:comment_leader = '"'
augroup END
endif
EDIT: I updated the ToggleComment function from my original answer. Most importantly, I modified the regular expressions. In many cases, it would not work correctly if there was another instance of a comment leader in the string. I believe it may have had to be preceded or followed by a space, but I can't remember. Either way, an example error case for Python is below.
print("Hello, World!") # Says hello to the world.
I've fixed this, and simplified the regular expressions a bit. One side effect is that it no longer adds a space after the comment leader, but this does not bother me. I've added a local declaration to a few variables that I forgot in my original answer.