9

I have defined an operator mapping which takes a region of text, then asks for an input string and then aligns the region with Tabular using the input string as an argument. It works nicely.

I have implemented it like this, using vim-operator-user to help defining a new operator:

map \aa <Plug>(operator-align)
call operator#user#define('align', 'Align')
function! Align(motion_wiseness)
    let expr = input("align: ")
    if len(expr) != 0
        execute "'[,']Tabularize /".expr
    endif
endfunction


function! Getchar()
    let c = getchar()
    if c =~ '^\d\+$'
        let c = nr2char(c)
    endif
    return c
endfunction

Then I wondered if I could make it update on the fly while entering the regular expression to align with. The problem with the current approach is that you have to undo and then redo if you are not using the correct expression.

For the interactive attempt, I made this:

map \== <Plug>(operator-align-interactive)
call operator#user#define('align-interactive', 'AlignInteractive')
function! AlignInteractive(motion_wiseness)
    let prompt = "Align: "
    echon prompt
    let expr = ""
    let c = Getchar()
     " CR has to be checked for separately as it acts as putting the cursor back to zero position
    while c != "\<Esc>" && c != "\<CR>"
        if c == "\<BS>"
            if len(expr) != 0
                let expr = expr[0:-2]
                echon "\<CR>".substitute(expr, ".", " ", "g")
                echon "\<CR>".prompt.expr
            endif
        else
            let expr .= c
            echon c
            let cmd = "'[,']Tabularize /".expr
            execute cmd 
        endif
        let c = Getchar()
    endwhile
endfunction

It should work but the aligning is not done before I hit enter, that is after I have finished entering input, effectively meaning that it works the same way as the non-interactive function. I wonder if the problem could be something like the screen / content not being updated during an operator execution, only after.

Any ideas on what the problem could be is appreciated!

8

You need to undo and redraw if you want to see changes in the buffer right away.

Here's what works:

function! AlignInteractive(motion_wiseness)
  let prompt = "Align: "
  let undo = 0
  let expr = ""
  let range = line("'[").','.line("']")
  let failed = 0
  let accept = 0

  echon prompt
  let c = Getchar()

  while c != "\<Esc>" && c != "\<c-c>"
    let undo = len(expr)

    if c == "\<CR>"
      let accept = 1
      break
    elseif c == "\<BS>"
      if len(expr)
        let expr = expr[0:-2]
      endif
    else
      let expr .= c
    endif

    if undo && !failed
      silent! undo
    endif

    if len(expr)
      try
        call match('', expr)
        let failed = 0
        execute range."Tabularize /".expr
      catch //
        let failed = 1
      endtry
    endif

    redraw

    echon prompt
    if failed
      echohl ErrorMsg
    endif
    echon expr
    echohl None
    let c = Getchar()
  endwhile

  if !accept && len(expr) && !failed
    silent! undo
  endif
endfunction

Explanation

The range variable stores the '[ and '] marks. This is for sanity's sake.

A variable called undo is set based on the previous length of expr. This means whenever there is input from the user, the next iteration can safely undo before executing Tabularize.

call match('', expr) tests the expression for pattern errors. If it fails, undo should not be executed. Pattern failures can happen as you're typing atoms such as \zs.

redraw will clear the command line. This is why the full prompt is printed on every iteration.

If the pattern contains an error, it is highlighted with ErrorMsg.

accept is set when <cr> is pressed. If it's false, undo the changes (if any). Anything else that breaks the loop cancels.

Existing plugin

There is a plugin called vim-easy-align that can do what you're attempting. You could draw inspiration from it's script.

  • Thank you so much for the clear explanations! I didn't know that easy-align could do that. The undo functionality was the next thing that I wanted to implement, but I got stuck at the updating. – tusj Jun 30 '16 at 15:31
  • No problem 🙂 I just updated the answer to highlight the pattern if it has an error. – Tommy A Jun 30 '16 at 15:40
  • 1
    There is a tiny bug in the current implementation, due to non-written requirement: if Esc or <C-C> has been typed, the operation should be cancelled. I have fixed the first case by adding: if c == "\<Esc>" && undo silent! undo endif I am not sure how to detect for <C-C> though. – tusj Jun 30 '16 at 17:02
  • @tusj I updated the answer. – Tommy A Jun 30 '16 at 18:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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