dmonopoly
dmonopoly

Reputation: 3331

Simple vim function from visual mode to toggle comments (learning exercise)

Partly for practice, and partly for personal use, I would like to be able to enter visual mode, select some lines, and then hit "," which would then toggle commenting. (I know NerdCommenter exists but I want something simple with nothing fancy - and again, this is practice, too.)

I've learned that you can do '&filetype' to access the filetype, and that '.' concatenates strings, ==# is case-sensitive string comparison, and =~ is regex matching. I also learned that getline('.') gets the line that visual mode has highlighted (each line if multiple lines are highlighted).

Here's my (flawed)

.vimrc

vnoremap , :call ToggleComment()<CR>
function! ToggleComment()
  "Choose comment string based on filetype.
  let comment_string = ''
  if &filetype ==# 'vim'
    let comment_string = '"'
  elseif &filetype ==# 'cpp'
    let comment_string = '\/\/'
  endif

  let line = getline('.')
  if line =~ '^' . comment_string
    "Comment.
    " How could I do the equivalent of "shift-I to go to the beginning of the
    " line, then enter comment_string"?
  else
    "Uncomment. This is flawed too. Maybe I should just go to the beginning of
    "the line and delete a number of characters over?
    execute 's/^' . comment_string . '//'
  endif
endfunction

One thing I get for the uncomment case, regardless of whether the line is commented or not, is

Pattern not found: ^"

(I tested on my vimrc file.)

Advice appreciated - I feel like this shouldn't be too complicated.

Upvotes: 0

Views: 184

Answers (2)

Birei
Birei

Reputation: 36272

You can use the range option after function declatarion, that lets you use two variables that contain the beginning and end of the range, a:firstline and a:lastline.

Inside an execute instruction prepend them to the substitution command to executes it only in that range. At the time of removing apply escape() to the variable to avoid collision of forward slashes:

function! ToggleComment() range
    let comment_string = ''
    if &filetype ==# 'vim'
        let comment_string = '"' 
    elseif &filetype ==# 'cpp'
        let comment_string = '//'
    endif
    let line = getline('.')
    if line =~ '^' . comment_string
        execute a:firstline . "," . a:lastline . 's/^' . escape(comment_string, '/') . '//'
    else
        execute a:firstline . "," . a:lastline . 's/^/\=printf( "%s", comment_string )/'
    endif
endfunction

UPDATE: To add and remove comments just before first non-blank character, you have to add optional spaces after the zero-width assertion of beginning of line. This is the part that changes. Note how I add \s* to comparison if it exists comments in that line and add \1 or submatch(1) in the replacement part of substitutions:

if line =~? '^\s*' . comment_string
    execute a:firstline . "," . a:lastline . 's/^\(\s*\)' . escape(comment_string, '/') . '/\1/'
else
    execute a:firstline . "," . a:lastline . 's/^\(\s*\)/\=submatch(1) . printf( "%s", comment_string )/'
endif

Upvotes: 2

Christian Brabandt
Christian Brabandt

Reputation: 8248

This probably fails, because your :s command doesn't find the comment string. You should probably use the e flag to your :s command (See also :h s_flags).

Additionally, I think you want to add a variable number of spaces between the line start and the comment string, e.g. something like if line =~ '^\s*'.comment so it does correctly catch:

#This is a comment
     #This is a comment

etc, you'll get the idea.

Upvotes: 0

Related Questions