1

This should be easy but it seems to be a blind spot in vim. I'm looking for a short one line mapping, let's say to <leader>r, to right-align text inside a visual block selection. For example, consider the following sample text:

a1   a
b22   b
c234   c
d4444   d

After, in a visual block, selecting exactly the first '1' to the last '4' (a total of 16 characters) as so

a||||a
b|||| b
c||||  c
d||||   d

and executing our command we should get:

a   1a
b  22 b
c 234  c
d4444   d

If we could pipe the block selection to the UNIX utility rs using rs -j 0 1 and replace the text in-place that would satisfy the problem. Presumably you should be able to delete (via d), operate on the register, and paste (via p), but I have not yet been able to get that to work.

Vim already has :right which works linewise but no mechanism that seems to work on selections (for more on why I consider this a blind spot see :h *:visual_example*). I have added the letters abcd to break solutions which only work linewise instead of selection-wise and to break solutions which hinge on a common separator. The 234 string breaks trivial mirrors which aren't right-aligns in general. I have already reviewed the various table, align, and justify plugins and am not interested in them as an answer. If at all possible functions should be avoided.

1
  • Those are probably fine answers to leave on the question for people who arrive here. For me personally I'm not interested in custom multi-line functions until it's demonstrated that nothing simpler (e.g., key combinations or one-line ":" style mappable commands) works. Commented Aug 7, 2017 at 0:25

3 Answers 3

6
+50

Here's a simple mapping to do the task:

vnoremap <silent> rs "zy:call setreg("z", system("echo \"" . @z . "\" \| sed 's/[ \\t]\\+//' \| rs -j 0 1"), "b")<CR>gv"zp

It uses z register to keep output of it's intermediate steps. I had to add call to sed to remove trailing white chars from the input to rs, because rs would produce strange output (maybe rs can be asked to do it by itself, but I couldn't figure it out myself)

2
  • 2
    Nice setreg(..., 'b'). I didn't know it. BTW, a pure vim script, and thus portable, solution would be: xnoremap µ y:call setreg('"', join(map(split(@", "\n"), "substitute(v:val, '\\v^(\\s*)(.{-})(\\s*)$', '\\1\\3\\2', '')"), "\n"), 'b')<cr>gvp Commented Aug 9, 2017 at 19:52
  • Cleaned up, no unescaped echo: vnoremap <silent> <leader>r "zd:call setreg("z", system("perl -ple 's/(.*\\S)(.*)/$2$1/'", getreg('z',1)),"b")<CR>"zP. For sed -E or sed -r use 's/(.*[^[:space:]])(.*)/\\2\\1/'. Commented Aug 9, 2017 at 20:09
3

What about doing the substitution on the visual selection with \%V, and avoiding external programs?

For instance, the following does the job (I'll use \v to simplify the patterns):

'<,'>s/\v%V(\s*)(\S*)(\s*)/\1\3\2/

However, it won't work with

a1    a
b2 2  b
c23 4 c
d444 4d

We cannot use \v%V(\s*)(.{-})(\s*)$ as $ denote the last position in the line, not in the selection :(

According to the doc,

'<,'>s/\v%V(\s*)(.{-})(\s*)%V/\1\3\2/

should work, but it doesn't while

'<,'>s/\v%V(\s*)(.{-})(\s*)%6c/\1\3\2/

does...

So, in order to get the exact end column (as '< and '> can be reversed), I have this solution.

exe "'<,'>".'s/\v%V(\s*)(.{-})(\s*)%'.(1+max([col("'<"), col("'>")])).'c/\1\3\2/'

BTW, we can also use :substitute and substitute():

 '<,'>s/\%V.*\%V./\=substitute(submatch(0), '\v^(\s*)(.{-})(\s*)$', '\1\3\2', '')

(along the way I think I've found out why I wasn't able to use a second \%V: it needs something behind. But I'm afraid the pattern will become quite complex)

EDIT4: The almost winning regex is

:'<,'>s/\v%V(.{-})(\s*)%V@!/\2\1/

but it fails with the new test case. Instead, the following seems to work:

:'<,'>s/\v%V(.*\S)(\s*%V\s)%V@!/\2\1/

And as a mapping it becomes:

:xnoremap µ :s/\v%V(.*\S)(\s*%V\s)%V@!/\2\1/<cr>

(Thank you @Christian Brabandt for the hint about /\%V) BTW, I don't need to match the leading spaces, it's even more simpler....

My understanding is the following: %V. says the next character belongs to the visual selection. %V@! says the next character shall not belong. Alas in \s*%V@!, we match as many spaces as possible as long as after the spaces we are outside the selection. This is not enough as it'll match spaces outside the selection as well. Hence my last edit with (\s*%V\s) where I request the last space to belong to the selection. If there is no space a the end of the selection, we won't match anything, but who care? This is already right aligned! And we still need a last %V@! to say after this last character we are outside the selection.

12
  • No, this: '<,'>s/\v%V(\s*)(.{-})(\s*)%V/\1\3\2/ won't work. Rather use something like this: '<,'>s/\%V\(\d\+\)\(\s\+\)\%V\@!/\2\1 which should match until the end of the visual selection. Commented Aug 9, 2017 at 19:50
  • @ChristianBrabandt. Thank out. BTW, it looks like /\%V entry could be improved in the documentation as this is not trivial. Commented Aug 9, 2017 at 20:10
  • Sure, patches are welcome :) Commented Aug 9, 2017 at 20:39
  • I played with this but couldn't get it to work. If you have a ready-to-go mapping I'll try that. Commented Aug 10, 2017 at 1:52
  • 1
    A mapping could be defined with: :xnoremap µ :s/\v%V(.{-})(\s*)%V@!/\2\1/<cr>. I've just tested fine on your example and on mine after having selected a visual block. Commented Aug 10, 2017 at 9:06
0

You and I seem to use different definitions of the word "simple".

You've stated that you're not interested in functions that solve the problem, but, for the benefit of other visitors to this page, here's a function (and mapping) that right aligns by moving spaces from the right side of the selection to the left-side:

function! RightAlign()
  " Get the line and column of the visual selection marks
  let [line1, column1] = getpos("'<")[1:2]
  let [line2, column2] = getpos("'>")[1:2]

  for l in range(line1, line2)
    " Get the line
    let line = getline(l)

    " Convert string to list for manipulation
    let line_list = split(line, '\zs')

    " Right align within the visual selection by moving spaces from right side
    " to left side
    while line_list[column2 - 1] =~ '\s'
      " Remove space from right side
      let space = remove(line_list, column2 - 1)
      " And add it to left side
      call insert(line_list, space, column1 - 1)
    endwhile

    " Convert list back to string and write out
    call setline(l, join(line_list, ""))
  endfor
endfunction
command! -range RightAlign call RightAlign()
xnoremap <leader>r :RightAlign<CR>

For me, this is simpler than the existing solutions on the page: it was completely straightforward to write, and I'm confident that if I came back to the code in a year or two I'd be able to figure out how it works in seconds. (Whereas, I still don't completely understand @grodzik and @LucHermitte's ingenious one-liners.)

Here's a similar version that works via normal mode commands which is possibly even simpler:

function! RightAlign2()
  " Get the line and column of the visual selection marks
  let [line1, column1] = getpos("'<")[1:2]
  let [line2, column2] = getpos("'>")[1:2]

  for l in range(line1, line2)
    " Right align within the visual selection by moving spaces from right side
    " to left side
    while getline(l)[column2 - 1] =~ '\s'
      " Jump to line
      execute "normal " . l . "G"
      " Jump to right
      execute "normal " . column2 . "|"
      " Cut
      normal x
      " Jump to left
      execute "normal " . column1 . "|"
      " Paste
      normal P
    endwhile
  endfor
endfunction

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