Vim Tips Wiki
Register
Advertisement
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

This tip is deprecated for the following reasons:

Vim 7.3.541 adds the 'j' flag to 'formatoptions' in order to accomplish the same thing as this tip. This tip is still useful for older versions, or to handle text not included in the comment leader like Vim line continuation lines, but with the latest version it should not be necessary.

Tip 1553 Printable Monobook Previous Next

created 2008 · complexity basic · author Fritzophrenic · version 7.0


Why remap J?

When editing code with comments in it (C code, Vim script, Python, or whatever) and you want to join two comment lines together, you may notice some annoying behavior.

For example:

" This is a Vim scripting language comment.
" I want to join this line
"      to this one

Performing a J on the second line will result in:

" This is a Vim scripting language comment.
" I want to join this line "      to this one

Ugh! Now you'll need to delete the comment leader and any internal indentation from the line! It gets even worse when giving J a count, or when using J in a large visual selection. You could end up with something like this:

" I wanted "          to join "         a lot "           of lines "     but this happened!

Luckily, Vim allows you to remap J to override the default functionality. But how to do it?

Visual mode mapping

Visual mode is easy. We know what text is selected by using '< and '>, and we know the comment leader, so we can simply apply a :s command to remove the comment leader before using J normally.

For Vim code, you could do it this way:

vnoremap <silent> <buffer> J :<C-U>'<+1,'>s/^\s*"\s*/<Space>/e<CR>gvJ

The breakdown:

  • :<C-U> – start an ex command from visual mode, which defaults to inserting '<,'> so we need to use CTRL-U to remove it
  • '<+1,'> – act on all but the first line of the visual selection (we don't want to remove the comment leader from the current line!)
  • s/^\s*"\s*/<Space>/e<CR> – replace all comment leaders that occur at the BEGINNING of a line, and all surrounding whitespace, with a single space. Use the e flag to suppress errors if the pattern is not found (so joins of non-comment lines continue to work). The <CR> runs the ex command.
  • gvJ – select the last visual selection range and perform a normal J command on it. Since we've removed the comment leaders, this will now work as desired.

This has the unfortunate side effect of clobbering your last search register, @/. This means (among other things) that the hlsearch highlighting will be changed after using J. This can be fixed by saving and restoring the @/ register using 'let' with a temporary variable, as seen later in this document.

Normal mode mapping

Unfortunately, normal mode is not nearly as easy as visual mode. The difficulty occurs because J in normal mode can take a count of the number of lines to join. For this, we need a function.

function! JoinWithLeader(count, leaderText)
  let l:linecount = a:count
  " default number of lines to join is 2
  if l:linecount < 2
    let l:linecount = 2
  endif
  echo l:linecount . " lines joined"
  " clear errmsg so we can determine if the search fails
  let v:errmsg = ''

  " save off the search register to restore it later because we will clobber
  " it with a substitute command
  let l:savsearch = @/

  while l:linecount > 1
    " do a J for each line (no mappings)
    normal! J
    " remove the comment leader from the current cursor position
    silent! execute 'substitute/\%#\s*\%('.a:leaderText.'\)\s*/ /'
    " check v:errmsg for status of the substitute command
    if v:errmsg=~'Pattern not found'
      " just means the line wasn't a comment - do nothing
    elseif v:errmsg!=''
      echo "Problem with leader pattern for JoinWithLeader()!"
    else
      " a successful substitute will move the cursor to line beginning,
      " so move it back
      normal! ``
    endif
    let l:linecount = l:linecount - 1
  endwhile
  " restore the @/ register
  let @/ = l:savsearch
endfunction

This function works by taking the number of lines, and using a regular old J command that many times, starting on the current line. The cool thing about J is that it places the cursor on the start of the joined line after it is done. For example:

" I joined two lines "    and look what happened!

Right after a J, the cursor will be on the 2nd " in the line.

The function above takes advantage of this fact by using the \%# atom in the substitute pattern to match the comment leader ONLY AT THE CURRENT CURSOR POSITION to allow us to remove it safely. After a successful substitute, we must move the cursor to its previous position, because a successful substitute will move it to the beginning of the line. However, since a failed substitute does not move the cursor, we much check the result of the substitute command to figure out whether to restore cursor position.

Whew! Now, to allow normal mode joining of comments, you can do this:

nnoremap <silent> <buffer> J :<C-U>call JoinWithLeader(v:count, '"')<CR>

In this mapping, we again use <C-U> to remove the count that automatically gets added to the beginning of an ex command given a count, and we then pass this count to our function using v:count.

Tying it all together

Now, we can individually map J in normal and visual mode to do what we want. But it would be nice to be able to do it more simply! This function allows a very easy mapping and adapts well to almost any language:

" Eliminate comment leader when joining comment lines
function! MapJoinWithLeaders(leaderText)
  let l:leaderText = escape(a:leaderText, '/')
  " visual mode is easy - just remove comment leaders from beginning of lines
  " before using J normally
  exec "vnoremap <silent> <buffer> J :<C-U>let savsearch=@/<Bar>'<+1,'>".
        \'s/^\s*\%('.
        \l:leaderText.
        \'\)\s*/<Space>/e<Bar>'.
        \'let @/=savsearch<Bar>unlet savsearch<CR>'.
        \'gvJ'
  " normal mode is harder because of the optional count - must use a function
  exec "nnoremap <silent> <buffer> J :<C-U>call JoinWithLeader(v:count, '".l:leaderText."')<CR>"
endfunction

Now, you can do cool things by calling this function in your $HOME/vimfiles/ftplugin/{filetype}.vim file!

For example, I have set up my J command to join Vim comments AND continued lines (using backslash) in Vim, by placing the following in $HOME/vimfiles/ftplugin/vim.vim:

" join comment lines and continued lines intelligently
call MapJoinWithLeaders('"\\|\\')

Note the use of \\| to allow matches on both \ and ".

Limitations

If you join a comment line to a non-comment line with a J command in normal mode, you will change something this...

  call func(l:args)
  " my comment

into this...

  call func(l:args) my comment

This is probably not what is desired! To avoid this, in situations like the above, instead of using J use :normal! J to avoid the mapping. Note that the mappings defined above are all undoable with u.

You can partially fix this problem by tweaking the substitution pattern to only take effect when there is a comment leader prior to the cursor in the line. However, this will break things like the Vim continuation lines above. If this does not matter to you, replace this:

    " remove the comment leader from the current cursor position
    silent! execute 'substitute/\%#\s*\%('.a:leaderText.'\)\s*/ /'

with this:

    " remove the comment leader from the current cursor position, but only if
    " the current line has a comment leader preceding this
    silent! execute 'substitute/\%('.a:leaderText.'\).\{-}\zs\%#\s*\%('.a:leaderText.'\)\s*/ /'

The problem will still occur when the comment leader occurs in non-comment text, but it should be far less common.

References

Comments

Advertisement