Improving our plugin

There are many paths we can take our plugin in, but let's focus on two main issues we have right now:

  • Our plugin fails with spectacular errors when we try to execute it in any other language
  • The plugin does not provide a way to operate on multiple lines at once

Let's start with the first problem: making the plugin work across different languages. Right now, if you try to execute the plugin in, for example, a .vim file, you'll get the following volley of errors:

That's because we define g:commenter#comment_str in <...>/vim-commenter/ftplugin/python.vim, and the variable is only defined when we're working with a Python file.

Vim syntax files define what the comments look like for each language, but they're not very consistent, and the logic to parse those and all the corner cases is outside of the scope of this book.

However, we can at least get rid of the nasty error, and make our own!

The canonical way of checking a variable exists is with exists. Let's add a new function to <...>/vim-commenter/autoload/commenter.vim, which would throw a custom error if g:commenter#comment_str is not set:

" Returns 1 if g:commenter#comment_str exists.
function! g:commenter#HasCommentStr()
if exists('g:commenter#comment_str')
return 1
endif
echom "vim-commenter doesn't work for filetype " . &ft . " yet"
return 0
endfunction

" Comment out the current line in Python.
function! g:commenter#ToggleComment()
if !g:commenter#HasCommentStr()
return
endif
let l:i = indent('.') " Number of spaces.
let l:line = getline('.')
let l:cur_row = getcurpos()[1]
let l:cur_col = getcurpos()[2]
let l:prefix = l:i > 0 ? l:line[:l:i - 1] : '' " Handle 0 indent cases.
if l:line[l:i:l:i + len(g:commenter#comment_str) - 1] ==#
g:commenter#comment_str
call setline('.', l:prefix .
l:line[l:i + len(g:commenter#comment_str):])
let l:cur_offset = -len(g:commenter#comment_str)
else
call setline('.', l:prefix . g:commenter#comment_str . l:line[l:i:])
let l:cur_offset = len(g:commenter#comment_str)
endif
call cursor(l:cur_row, l:cur_col + l:cur_offset)
endfunction

Now, we get a message when we try to comment out a line in a non-Python file:

A much better user experience if you ask me.

And now, let's add a way for our plugin to be invoked on multiple lines. The easiest thing to do would be to allow the user to prefix our gc command with a number, and we'll do just that.

Vim lets you access a number that prefixes a mapping by using v:count. Even better, there's a v:count1, which defaults to 1 if no count was given (this way, we can reuse more of our code).

Let's update our mapping in <...>/vim-commenter/plugin/commenter.vim:

nnoremap gc :<c-u>call g:commenter#ToggleComment(v:count1)<cr>

<c-u> is required to be used with v:count and v:count1. You can check :help v:count or :help v:count1 for an explanation.

In fact, we can also add a visual mode mapping to support visual selection:

vnoremap gc :<cu>call g:commenter#ToggleComment(
line("'>") - line("'<") + 1)<cr>
line("'>") gets the line number of the end of the selection, while line("'<") gets the line number of the beginning of the selection. Subtract the beginning line number from the end, add one, and we have ourselves a line count!

Now, let's update <...>/vim-commenter/autoload/commenter.vim with a few new methods:

" Returns 1 if g:commenter#comment_str exists.
function! g:commenter#HasCommentStr()
if exists('g:commenter#comment_str')
return 1
endif
echom "vim-commenter doesn't work for filetype " . &ft . " yet"
return 0
endfunction

" Detect smallest indentation for a range of lines.
function! g:commenter#DetectMinIndent(start, end)
let l:min_indent = -1
let l:i = a:start
while l:i <= a:end
if l:min_indent == -1 || indent(l:i) < l:min_indent
let l:min_indent = indent(l:i)
endif
let l:i += 1
endwhile
return l:min_indent
endfunction

function! g:commenter#InsertOrRemoveComment(lnum, line, indent, is_insert)
" Handle 0 indent cases.
let l:prefix = a:indent > 0 ? a:line[:a:indent - 1] : ''
if a:is_insert
call setline(a:lnum, l:prefix . g:commenter#comment_str .
a:line[a:indent:])
else
call setline(
a:lnum, l:prefix . a:line[a:indent + len(g:commenter#comment_str):]
endif
endfunction

" Comment out the current line in Python.
function! g:commenter#ToggleComment(count)
if !g:commenter#HasCommentStr()
return
endif
let l:start = line('.')
let l:end = l:start + a:count - 1
if l:end > line('$') " Stop at the end of file.
let l:end = line('$')
endif
let l:indent = g:commenter#DetectMinIndent(l:start, l:end)
let l:lines = l:start == l:end ?
[getline(l:start)] : getline(l:start, l:end)
let l:cur_row = getcurpos()[1]
let l:cur_col = getcurpos()[2]
let l:lnum = l:start
if l:lines[0][l:indent:l:indent + len(g:commenter#comment_str) - 1] ==#
g:commenter#comment_str
let l:is_insert = 0
let l:cur_offset = -len(g:commenter#comment_str)
else
let l:is_insert = 1
let l:cur_offset = len(g:commenter#comment_str)
endif
for l:line in l:lines
call g:commenter#InsertOrRemoveComment(
l:lnum, l:line, l:indent, l:is_insert)
let l:lnum += 1
endfor
call cursor(l:cur_row, l:cur_col + l:cur_offset)
endfunction

This script is now much bigger, but it's not as scary as it looks! Here, we've added two new functions:

  • g:commenter#DetectMinIndent finds the smallest indent within a given range. This way, we make sure to indent the outermost section of code.
  • g:commenter#InsertOrRemoveComment either inserts or removes a comment within a given line and at a given indentation level.

Let's test run our plugin. Let's, say, run it with 11gc:

Ta-da! Now, our little plugin can comment out multiple lines! Give it a go with a few more tries to make sure we covered corner cases such as commenting in the visual mode going past the end of the file, commenting and uncommenting a single line, and so on.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.35.148