0

Vim indents Ruby multiline method-chains like:

object.read
  .write
  .rithmetic

I'd like to make it align the dots instead:

object.read
      .write
      .rithmetic

I haven't been able to find a relevant setting in any of $VIMRUNTIME/indent/ruby.vim, $VIMRUNTIME/syntax/ruby.vim, or https://github.com/vim-ruby/vim-ruby

Is this configurable, or do I need to write my own customization?


Update: I borrowed some ideas from the answer below in an attempt to write a solution that doesn't move the cursor or generate multiple undo-blocks. It works quite well when triggered manually. Wiring it into an insert-mode . mapping is proving tricky/buggy, but I'm still fiddling with it.

func! realign#ruby_method_chain()
  if s:has_leading_dot()
    call s:align_method_chain_up()
  endif
  if line('.') < line('$')
    call s:align_method_chain_down()
  endif
endf

func! s:has_leading_dot()
  return match(getline('.'), '^\s*\zs\.') != -1
endf

func! s:align_method_chain_up()
  let [dot_line, dot_col] = s:chainstart()
  if dot_line
    call s:align_range(dot_line + 1, line('.'), dot_col)
  endif
endf

func! s:chainstart()
  let before_line = '\%<'. line('.') .'l'
  let dot = s:backsearch_chainstart(before_line, '')

  while dot[0] && s:is_comment(dot)
    let before_col = '\%<'. dot[1] .'c'
    let dot = s:backsearch_chainstart(before_line, before_col)
  endwhile

  return dot
endf

func! s:backsearch_chainstart(line_anchor, col_anchor)
  return searchpos(s:chainstart_regex(a:line_anchor, a:col_anchor), 'bWn') " backwards, no-wrap, no-move
endf

func! s:chainstart_regex(line, col)
  return '^\s*[^[:blank:].].*' . a:line . a:col . '\zs\.\ze\K'
endf

func! s:is_comment(pos)
  return synIDattr(synID(a:pos[0], a:pos[1], 1), 'name') == 'rubyComment'
endf

func! s:align_range(minline, maxline, col)
  if a:minline <= a:maxline
    execute a:minline.','.a:maxline 'left' a:col - 1
  endif
endf

func! s:align_method_chain_down()
  let [curline, dot_col] = s:chaindot()
  if dot_col
    let maxline = s:chain_maxline()
    call s:align_range(curline + 1, maxline, dot_col)
  endif
endf

func! s:chaindot()
  let curline = line('.')
  let linestr = getline(curline)
  let dot_idx = match(linestr, '.*\zs\.\ze\K.*$')

  while dot_idx != -1 && s:is_comment([curline, dot_idx + 1])
    let linestr = linestr[:(dot_idx - 1)]
    let dot_idx = match(linestr, '.*\zs\.\ze\K.*$')
  endwhile

  return [curline, dot_idx + 1]
endf

func! s:chain_maxline()
  let pattern = '\%>'.line('.').'l^\s*\([^[:blank:].]\+.*\)\?$'
  let line_beyond = search(pattern, 'nW')
  return line_beyond ? line_beyond - 1 : line('$')
endf

1 Answer 1

0

I came up with the following. Put this inside ~/.vim/after/ftplugin/ruby.vim

inoremap <buffer> <silent> . .<esc>:call <sid>electric_dot()<cr>a

function! s:electric_dot()
  let lnum = line('.')
  if getline(lnum) !~ '^\s*\.$'
    return
  endif

  let pat = '^\s*\.'
  let start = s:until(pat, 'b')
  execute lnum

  if start == 0 || getline(start-1) !~ '^\s*\w.*\.\w'
    normal! ^
    return
  endif

  let end = s:until(pat, '')
  let indent = len(matchstr(getline(start-1), '.*\ze\.'))

  execute start.','.end.'left'.indent
  execute lnum
  normal! ^
endfunction

function! s:until(pat, flags)
  let lnum = line('.')
  let [temp,_] = searchpos(a:pat, a:flags.'W')
  while abs(temp - lnum) == 1
    let lnum = temp
    let [temp,_] = searchpos(a:pat, a:flags.'W')
  endwhile
  return lnum
endfunction

Basically this creates a mapping on . in insert mode which finds the closest block of ^\s*\. text. Using this it looks to see if the line before the block matches your object.method style. If it does then it indents the block to the level of the last .. Then jumps back into insert mode.

Notes:

  • Depends on set expandtab, so no tabs
  • Naive method to search for last method call, so a . in a parameter will throw this off
  • Depends on set magic. Could update this with \m to remove this.
  • Has no understanding of ruby parsing rules so probably misbehaves in some cases.
  • This was a fun exercise for me. I have not plans on maintain this. Do not blame me if it messes up your code, destroys your computer, runs off with your significant other, or any other bad behavior.
1
  • Thanks. It looks like implementing this via the actual indentation or syntax file is non-trivial. I borrowed some ideas from your answer to write something I can call on demand. Wiring my version into insert mode is proving difficult/buggy so far but I'll keep fiddling with it. I don't use this indentation-style most of the time, so a manually-triggered solution is not so bad. I wanted a solution that wouldn't move the cursor or interrupt the undo-block. I'll post what I came up with above.
    – ivan
    Commented Jul 24, 2017 at 12:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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