neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

ruby.vim (31174B)


      1 " Vim indent file
      2 " Language:		Ruby
      3 " Maintainer:		Andrew Radev <andrey.radev@gmail.com>
      4 " Previous Maintainer:	Nikolai Weibull <now at bitwi.se>
      5 " URL:			https://github.com/vim-ruby/vim-ruby
      6 " Last Change:		2023 Dec 22
      7 
      8 " 0. Initialization {{{1
      9 " =================
     10 
     11 " Only load this indent file when no other was loaded.
     12 if exists("b:did_indent")
     13  finish
     14 endif
     15 let b:did_indent = 1
     16 
     17 if !exists('g:ruby_indent_access_modifier_style')
     18  " Possible values: "normal", "indent", "outdent"
     19  let g:ruby_indent_access_modifier_style = 'normal'
     20 endif
     21 
     22 if !exists('g:ruby_indent_assignment_style')
     23  " Possible values: "variable", "hanging"
     24  let g:ruby_indent_assignment_style = 'hanging'
     25 endif
     26 
     27 if !exists('g:ruby_indent_block_style')
     28  " Possible values: "expression", "do"
     29  let g:ruby_indent_block_style = 'do'
     30 endif
     31 
     32 if !exists('g:ruby_indent_hanging_elements')
     33  " Non-zero means hanging indents are enabled, zero means disabled
     34  let g:ruby_indent_hanging_elements = 1
     35 endif
     36 
     37 setlocal nosmartindent
     38 
     39 " Now, set up our indentation expression and keys that trigger it.
     40 setlocal indentexpr=GetRubyIndent(v:lnum)
     41 setlocal indentkeys=0{,0},0),0],!^F,o,O,e,:,.
     42 setlocal indentkeys+==end,=else,=elsif,=when,=in\ ,=ensure,=rescue,==begin,==end
     43 setlocal indentkeys+==private,=protected,=public
     44 
     45 let b:undo_indent = "setlocal indentexpr< indentkeys< smartindent<"
     46 
     47 " Only define the function once.
     48 if exists("*GetRubyIndent")
     49  finish
     50 endif
     51 
     52 let s:cpo_save = &cpo
     53 set cpo&vim
     54 
     55 " 1. Variables {{{1
     56 " ============
     57 
     58 " Syntax group names that are strings.
     59 let s:syng_string =
     60      \ ['String', 'Interpolation', 'InterpolationDelimiter', 'StringEscape']
     61 
     62 " Syntax group names that are strings or documentation.
     63 let s:syng_stringdoc = s:syng_string + ['Documentation']
     64 
     65 " Syntax group names that are or delimit strings/symbols/regexes or are comments.
     66 let s:syng_strcom = s:syng_stringdoc + [
     67      \ 'Character',
     68      \ 'Comment',
     69      \ 'HeredocDelimiter',
     70      \ 'PercentRegexpDelimiter',
     71      \ 'PercentStringDelimiter',
     72      \ 'PercentSymbolDelimiter',
     73      \ 'Regexp',
     74      \ 'RegexpCharClass',
     75      \ 'RegexpDelimiter',
     76      \ 'RegexpEscape',
     77      \ 'StringDelimiter',
     78      \ 'Symbol',
     79      \ 'SymbolDelimiter',
     80      \ ]
     81 
     82 " Expression used to check whether we should skip a match with searchpair().
     83 let s:skip_expr =
     84      \ 'index(map('.string(s:syng_strcom).',"hlID(''ruby''.v:val)"), synID(line("."),col("."),1)) >= 0'
     85 
     86 " Regex used for words that, at the start of a line, add a level of indent.
     87 let s:ruby_indent_keywords =
     88      \ '^\s*\zs\<\%(module\|class\|if\|for' .
     89      \   '\|while\|until\|else\|elsif\|case\|when\|in\|unless\|begin\|ensure\|rescue' .
     90      \   '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' .
     91      \ '\|\%([=,*/%+-]\|<<\|>>\|:\s\)\s*\zs' .
     92      \    '\<\%(if\|for\|while\|until\|case\|unless\|begin\):\@!\>'
     93 
     94 " Def without an end clause: def method_call(...) = <expression>
     95 let s:ruby_endless_def =
     96      \ '\<def\s\+\%(\k\+\.\)\=\%(\k\+[=!?]\=\|' .
     97      \ '[-+*/%&^<>~!]\|' .
     98      \ '\*\*\|>>\|<<\|' .
     99      \ '===\?\|\!=\|=\~\|\!\~\|' .
    100      \ '<=>\|<=\|>=\|' .
    101      \ '[-+!\~]@\|\[\]' .
    102      \ '\)\%((.*)\|\s\)\s*='
    103 
    104 " Regex used for words that, at the start of a line, remove a level of indent.
    105 let s:ruby_deindent_keywords =
    106      \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|in\|end\):\@!\>'
    107 
    108 " Regex that defines the start-match for the 'end' keyword.
    109 "let s:end_start_regex = '\%(^\|[^.]\)\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\|do\)\>'
    110 " TODO: the do here should be restricted somewhat (only at end of line)?
    111 let s:end_start_regex =
    112      \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
    113      \ '\<\%(module\|class\|if\|for\|while\|until\|case\|unless\|begin' .
    114      \   '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' .
    115      \ '\|\%(^\|[^.:@$]\)\@<=\<do:\@!\>'
    116 
    117 " Regex that defines the middle-match for the 'end' keyword.
    118 let s:end_middle_regex = '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue:\@!\>\|when\|\%(\%(^\|;\)\s*\)\@<=\<in\|elsif\):\@!\>'
    119 
    120 " Regex that defines the end-match for the 'end' keyword.
    121 let s:end_end_regex = '\%(^\|[^.:@$]\)\@<=\<end:\@!\>'
    122 
    123 " Expression used for searchpair() call for finding a match for an 'end' keyword.
    124 function! s:EndSkipExpr()
    125  if eval(s:skip_expr)
    126    return 1
    127  elseif expand('<cword>') == 'do'
    128        \ && getline(".") =~ '^\s*\<\(while\|until\|for\):\@!\>'
    129    return 1
    130  elseif getline('.') =~ s:ruby_endless_def
    131    return 1
    132  elseif getline('.') =~ '\<def\s\+\k\+[!?]\=([^)]*$'
    133    " Then it's a `def method(` with a possible `) =` later
    134    call search('\<def\s\+\k\+\zs(', 'W', line('.'))
    135    normal! %
    136    return getline('.') =~ ')\s*='
    137  else
    138    return 0
    139  endif
    140 endfunction
    141 
    142 let s:end_skip_expr = function('s:EndSkipExpr')
    143 
    144 " Regex that defines continuation lines, not including (, {, or [.
    145 let s:non_bracket_continuation_regex =
    146      \ '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$'
    147 
    148 " Regex that defines continuation lines.
    149 let s:continuation_regex =
    150      \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$'
    151 
    152 " Regex that defines continuable keywords
    153 let s:continuable_regex =
    154      \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
    155      \ '\<\%(if\|for\|while\|until\|unless\):\@!\>'
    156 
    157 " Regex that defines bracket continuations
    158 let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$'
    159 
    160 " Regex that defines dot continuations
    161 let s:dot_continuation_regex = '%\@<!\.\s*\%(#.*\)\=$'
    162 
    163 " Regex that defines backslash continuations
    164 let s:backslash_continuation_regex = '%\@<!\\\s*$'
    165 
    166 " Regex that defines end of bracket continuation followed by another continuation
    167 let s:bracket_switch_continuation_regex = '^\([^(]\+\zs).\+\)\+'.s:continuation_regex
    168 
    169 " Regex that defines the first part of a splat pattern
    170 let s:splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$'
    171 
    172 " Regex that describes all indent access modifiers
    173 let s:access_modifier_regex = '\C^\s*\%(public\|protected\|private\)\s*\%(#.*\)\=$'
    174 
    175 " Regex that describes the indent access modifiers (excludes public)
    176 let s:indent_access_modifier_regex = '\C^\s*\%(protected\|private\)\s*\%(#.*\)\=$'
    177 
    178 " Regex that defines blocks.
    179 "
    180 " Note that there's a slight problem with this regex and s:continuation_regex.
    181 " Code like this will be matched by both:
    182 "
    183 "   method_call do |(a, b)|
    184 "
    185 " The reason is that the pipe matches a hanging "|" operator.
    186 "
    187 let s:block_regex =
    188      \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|[^|]*|\)\=\s*\%(#.*\)\=$'
    189 
    190 let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex
    191 
    192 " Regex that describes a leading operator (only a method call's dot for now)
    193 let s:leading_operator_regex = '^\s*\%(&\=\.\)'
    194 
    195 " 2. GetRubyIndent Function {{{1
    196 " =========================
    197 
    198 function! GetRubyIndent(...) abort
    199  " 2.1. Setup {{{2
    200  " ----------
    201 
    202  let indent_info = {}
    203 
    204  " The value of a single shift-width
    205  if exists('*shiftwidth')
    206    let indent_info.sw = shiftwidth()
    207  else
    208    let indent_info.sw = &sw
    209  endif
    210 
    211  " For the current line, use the first argument if given, else v:lnum
    212  let indent_info.clnum = a:0 ? a:1 : v:lnum
    213  let indent_info.cline = getline(indent_info.clnum)
    214 
    215  " Set up variables for restoring position in file.  Could use clnum here.
    216  let indent_info.col = col('.')
    217 
    218  " 2.2. Work on the current line {{{2
    219  " -----------------------------
    220  let indent_callback_names = [
    221        \ 's:AccessModifier',
    222        \ 's:ClosingBracketOnEmptyLine',
    223        \ 's:BlockComment',
    224        \ 's:DeindentingKeyword',
    225        \ 's:MultilineStringOrLineComment',
    226        \ 's:ClosingHeredocDelimiter',
    227        \ 's:LeadingOperator',
    228        \ ]
    229 
    230  for callback_name in indent_callback_names
    231 "    Decho "Running: ".callback_name
    232    let indent = call(function(callback_name), [indent_info])
    233 
    234    if indent >= 0
    235 "      Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
    236      return indent
    237    endif
    238  endfor
    239 
    240  " 2.3. Work on the previous line. {{{2
    241  " -------------------------------
    242 
    243  " Special case: we don't need the real s:PrevNonBlankNonString for an empty
    244  " line inside a string. And that call can be quite expensive in that
    245  " particular situation.
    246  let indent_callback_names = [
    247        \ 's:EmptyInsideString',
    248        \ ]
    249 
    250  for callback_name in indent_callback_names
    251 "    Decho "Running: ".callback_name
    252    let indent = call(function(callback_name), [indent_info])
    253 
    254    if indent >= 0
    255 "      Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
    256      return indent
    257    endif
    258  endfor
    259 
    260  " Previous line number
    261  let indent_info.plnum = s:PrevNonBlankNonString(indent_info.clnum - 1)
    262  let indent_info.pline = getline(indent_info.plnum)
    263 
    264  let indent_callback_names = [
    265        \ 's:StartOfFile',
    266        \ 's:AfterAccessModifier',
    267        \ 's:ContinuedLine',
    268        \ 's:AfterBlockOpening',
    269        \ 's:AfterHangingSplat',
    270        \ 's:AfterUnbalancedBracket',
    271        \ 's:AfterLeadingOperator',
    272        \ 's:AfterEndKeyword',
    273        \ 's:AfterIndentKeyword',
    274        \ ]
    275 
    276  for callback_name in indent_callback_names
    277 "    Decho "Running: ".callback_name
    278    let indent = call(function(callback_name), [indent_info])
    279 
    280    if indent >= 0
    281 "      Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
    282      return indent
    283    endif
    284  endfor
    285 
    286  " 2.4. Work on the MSL line. {{{2
    287  " --------------------------
    288  let indent_callback_names = [
    289        \ 's:PreviousNotMSL',
    290        \ 's:IndentingKeywordInMSL',
    291        \ 's:ContinuedHangingOperator',
    292        \ ]
    293 
    294  " Most Significant line based on the previous one -- in case it's a
    295  " continuation of something above
    296  let indent_info.plnum_msl = s:GetMSL(indent_info.plnum)
    297 
    298  for callback_name in indent_callback_names
    299 "    Decho "Running: ".callback_name
    300    let indent = call(function(callback_name), [indent_info])
    301 
    302    if indent >= 0
    303 "      Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
    304      return indent
    305    endif
    306  endfor
    307 
    308  " }}}2
    309 
    310  " By default, just return the previous line's indent
    311 "  Decho "Default case matched"
    312  return indent(indent_info.plnum)
    313 endfunction
    314 
    315 " 3. Indenting Logic Callbacks {{{1
    316 " ============================
    317 
    318 function! s:AccessModifier(cline_info) abort
    319  let info = a:cline_info
    320 
    321  " If this line is an access modifier keyword, align according to the closest
    322  " class declaration.
    323  if g:ruby_indent_access_modifier_style == 'indent'
    324    if s:Match(info.clnum, s:access_modifier_regex)
    325      let class_lnum = s:FindContainingClass()
    326      if class_lnum > 0
    327        return indent(class_lnum) + info.sw
    328      endif
    329    endif
    330  elseif g:ruby_indent_access_modifier_style == 'outdent'
    331    if s:Match(info.clnum, s:access_modifier_regex)
    332      let class_lnum = s:FindContainingClass()
    333      if class_lnum > 0
    334        return indent(class_lnum)
    335      endif
    336    endif
    337  endif
    338 
    339  return -1
    340 endfunction
    341 
    342 function! s:ClosingBracketOnEmptyLine(cline_info) abort
    343  let info = a:cline_info
    344 
    345  " If we got a closing bracket on an empty line, find its match and indent
    346  " according to it.  For parentheses we indent to its column - 1, for the
    347  " others we indent to the containing line's MSL's level.  Return -1 if fail.
    348  let col = matchend(info.cline, '^\s*[]})]')
    349 
    350  if col > 0 && !s:IsInStringOrComment(info.clnum, col)
    351    call cursor(info.clnum, col)
    352    let closing_bracket = info.cline[col - 1]
    353    let bracket_pair = strpart('(){}[]', stridx(')}]', closing_bracket) * 2, 2)
    354 
    355    if searchpair(escape(bracket_pair[0], '\['), '', bracket_pair[1], 'bW', s:skip_expr) > 0
    356      if closing_bracket == ')' && col('.') != col('$') - 1
    357        if g:ruby_indent_hanging_elements
    358          let ind = virtcol('.') - 1
    359        else
    360          let ind = indent(line('.'))
    361        end
    362      elseif g:ruby_indent_block_style == 'do'
    363        let ind = indent(line('.'))
    364      else " g:ruby_indent_block_style == 'expression'
    365        let ind = indent(s:GetMSL(line('.')))
    366      endif
    367    endif
    368 
    369    return ind
    370  endif
    371 
    372  return -1
    373 endfunction
    374 
    375 function! s:BlockComment(cline_info) abort
    376  " If we have a =begin or =end set indent to first column.
    377  if match(a:cline_info.cline, '^\s*\%(=begin\|=end\)$') != -1
    378    return 0
    379  endif
    380  return -1
    381 endfunction
    382 
    383 function! s:DeindentingKeyword(cline_info) abort
    384  let info = a:cline_info
    385 
    386  " If we have a deindenting keyword, find its match and indent to its level.
    387  " TODO: this is messy
    388  if s:Match(info.clnum, s:ruby_deindent_keywords)
    389    call cursor(info.clnum, 1)
    390 
    391    if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
    392          \ s:end_skip_expr) > 0
    393      let msl  = s:GetMSL(line('.'))
    394      let line = getline(line('.'))
    395 
    396      if s:IsAssignment(line, col('.')) &&
    397            \ strpart(line, col('.') - 1, 2) !~ 'do'
    398        " assignment to case/begin/etc, on the same line
    399        if g:ruby_indent_assignment_style == 'hanging'
    400          " hanging indent
    401          let ind = virtcol('.') - 1
    402        else
    403          " align with variable
    404          let ind = indent(line('.'))
    405        endif
    406      elseif g:ruby_indent_block_style == 'do'
    407        " align to line of the "do", not to the MSL
    408        let ind = indent(line('.'))
    409      elseif getline(msl) =~ '=\s*\(#.*\)\=$'
    410        " in the case of assignment to the MSL, align to the starting line,
    411        " not to the MSL
    412        let ind = indent(line('.'))
    413      else
    414        " align to the MSL
    415        let ind = indent(msl)
    416      endif
    417    endif
    418    return ind
    419  endif
    420 
    421  return -1
    422 endfunction
    423 
    424 function! s:MultilineStringOrLineComment(cline_info) abort
    425  let info = a:cline_info
    426 
    427  " If we are in a multi-line string or line-comment, don't do anything to it.
    428  if s:IsInStringOrDocumentation(info.clnum, matchend(info.cline, '^\s*') + 1)
    429    return indent(info.clnum)
    430  endif
    431  return -1
    432 endfunction
    433 
    434 function! s:ClosingHeredocDelimiter(cline_info) abort
    435  let info = a:cline_info
    436 
    437  " If we are at the closing delimiter of a "<<" heredoc-style string, set the
    438  " indent to 0.
    439  if info.cline =~ '^\k\+\s*$'
    440        \ && s:IsInStringDelimiter(info.clnum, 1)
    441        \ && search('\V<<'.info.cline, 'nbW') > 0
    442    return 0
    443  endif
    444 
    445  return -1
    446 endfunction
    447 
    448 function! s:LeadingOperator(cline_info) abort
    449  " If the current line starts with a leading operator, add a level of indent.
    450  if s:Match(a:cline_info.clnum, s:leading_operator_regex)
    451    return indent(s:GetMSL(a:cline_info.clnum)) + a:cline_info.sw
    452  endif
    453  return -1
    454 endfunction
    455 
    456 function! s:EmptyInsideString(pline_info) abort
    457  " If the line is empty and inside a string (the previous line is a string,
    458  " too), use the previous line's indent
    459  let info = a:pline_info
    460 
    461  let plnum = prevnonblank(info.clnum - 1)
    462  let pline = getline(plnum)
    463 
    464  if info.cline =~ '^\s*$'
    465        \ && s:IsInStringOrComment(plnum, 1)
    466        \ && s:IsInStringOrComment(plnum, strlen(pline))
    467    return indent(plnum)
    468  endif
    469  return -1
    470 endfunction
    471 
    472 function! s:StartOfFile(pline_info) abort
    473  " At the start of the file use zero indent.
    474  if a:pline_info.plnum == 0
    475    return 0
    476  endif
    477  return -1
    478 endfunction
    479 
    480 function! s:AfterAccessModifier(pline_info) abort
    481  let info = a:pline_info
    482 
    483  if g:ruby_indent_access_modifier_style == 'indent'
    484    " If the previous line was a private/protected keyword, add a
    485    " level of indent.
    486    if s:Match(info.plnum, s:indent_access_modifier_regex)
    487      return indent(info.plnum) + info.sw
    488    endif
    489  elseif g:ruby_indent_access_modifier_style == 'outdent'
    490    " If the previous line was a private/protected/public keyword, add
    491    " a level of indent, since the keyword has been out-dented.
    492    if s:Match(info.plnum, s:access_modifier_regex)
    493      return indent(info.plnum) + info.sw
    494    endif
    495  endif
    496  return -1
    497 endfunction
    498 
    499 " Example:
    500 "
    501 "   if foo || bar ||
    502 "       baz || bing
    503 "     puts "foo"
    504 "   end
    505 "
    506 function! s:ContinuedLine(pline_info) abort
    507  let info = a:pline_info
    508 
    509  let col = s:Match(info.plnum, s:ruby_indent_keywords)
    510  if s:Match(info.plnum, s:continuable_regex) &&
    511        \ s:Match(info.plnum, s:continuation_regex)
    512    if col > 0 && s:IsAssignment(info.pline, col)
    513      if g:ruby_indent_assignment_style == 'hanging'
    514        " hanging indent
    515        let ind = col - 1
    516      else
    517        " align with variable
    518        let ind = indent(info.plnum)
    519      endif
    520    else
    521      let ind = indent(s:GetMSL(info.plnum))
    522    endif
    523    return ind + info.sw + info.sw
    524  endif
    525  return -1
    526 endfunction
    527 
    528 function! s:AfterBlockOpening(pline_info) abort
    529  let info = a:pline_info
    530 
    531  " If the previous line ended with a block opening, add a level of indent.
    532  if s:Match(info.plnum, s:block_regex)
    533    if g:ruby_indent_block_style == 'do'
    534      " don't align to the msl, align to the "do"
    535      let ind = indent(info.plnum) + info.sw
    536    else
    537      let plnum_msl = s:GetMSL(info.plnum)
    538 
    539      if getline(plnum_msl) =~ '=\s*\(#.*\)\=$'
    540        " in the case of assignment to the msl, align to the starting line,
    541        " not to the msl
    542        let ind = indent(info.plnum) + info.sw
    543      else
    544        let ind = indent(plnum_msl) + info.sw
    545      endif
    546    endif
    547 
    548    return ind
    549  endif
    550 
    551  return -1
    552 endfunction
    553 
    554 function! s:AfterLeadingOperator(pline_info) abort
    555  " If the previous line started with a leading operator, use its MSL's level
    556  " of indent
    557  if s:Match(a:pline_info.plnum, s:leading_operator_regex)
    558    return indent(s:GetMSL(a:pline_info.plnum))
    559  endif
    560  return -1
    561 endfunction
    562 
    563 function! s:AfterHangingSplat(pline_info) abort
    564  let info = a:pline_info
    565 
    566  " If the previous line ended with the "*" of a splat, add a level of indent
    567  if info.pline =~ s:splat_regex
    568    return indent(info.plnum) + info.sw
    569  endif
    570  return -1
    571 endfunction
    572 
    573 function! s:AfterUnbalancedBracket(pline_info) abort
    574  let info = a:pline_info
    575 
    576  " If the previous line contained unclosed opening brackets and we are still
    577  " in them, find the rightmost one and add indent depending on the bracket
    578  " type.
    579  "
    580  " If it contained hanging closing brackets, find the rightmost one, find its
    581  " match and indent according to that.
    582  if info.pline =~ '[[({]' || info.pline =~ '[])}]\s*\%(#.*\)\=$'
    583    let [opening, closing] = s:ExtraBrackets(info.plnum)
    584 
    585    if opening.pos != -1
    586      if !g:ruby_indent_hanging_elements
    587        return indent(info.plnum) + info.sw
    588      elseif opening.type == '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
    589        if col('.') + 1 == col('$')
    590          return indent(info.plnum) + info.sw
    591        else
    592          return virtcol('.')
    593        endif
    594      else
    595        let nonspace = matchend(info.pline, '\S', opening.pos + 1) - 1
    596        return nonspace > 0 ? nonspace : indent(info.plnum) + info.sw
    597      endif
    598    elseif closing.pos != -1
    599      call cursor(info.plnum, closing.pos + 1)
    600      normal! %
    601 
    602      if strpart(info.pline, closing.pos) =~ '^)\s*='
    603        " special case: the closing `) =` of an endless def
    604        return indent(s:GetMSL(line('.')))
    605      endif
    606 
    607      if s:Match(line('.'), s:ruby_indent_keywords)
    608        return indent('.') + info.sw
    609      else
    610        return indent(s:GetMSL(line('.')))
    611      endif
    612    else
    613      call cursor(info.clnum, info.col)
    614    end
    615  endif
    616 
    617  return -1
    618 endfunction
    619 
    620 function! s:AfterEndKeyword(pline_info) abort
    621  let info = a:pline_info
    622  " If the previous line ended with an "end", match that "end"s beginning's
    623  " indent.
    624  let col = s:Match(info.plnum, '\%(^\|[^.:@$]\)\<end\>\s*\%(#.*\)\=$')
    625  if col > 0
    626    call cursor(info.plnum, col)
    627    if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW',
    628          \ s:end_skip_expr) > 0
    629      let n = line('.')
    630      let ind = indent('.')
    631      let msl = s:GetMSL(n)
    632      if msl != n
    633        let ind = indent(msl)
    634      end
    635      return ind
    636    endif
    637  end
    638  return -1
    639 endfunction
    640 
    641 function! s:AfterIndentKeyword(pline_info) abort
    642  let info = a:pline_info
    643  let col = s:Match(info.plnum, s:ruby_indent_keywords)
    644 
    645  if col > 0 && s:Match(info.plnum, s:ruby_endless_def) <= 0
    646    call cursor(info.plnum, col)
    647    let ind = virtcol('.') - 1 + info.sw
    648    " TODO: make this better (we need to count them) (or, if a searchpair
    649    " fails, we know that something is lacking an end and thus we indent a
    650    " level
    651    if s:Match(info.plnum, s:end_end_regex)
    652      let ind = indent('.')
    653    elseif s:IsAssignment(info.pline, col)
    654      if g:ruby_indent_assignment_style == 'hanging'
    655        " hanging indent
    656        let ind = col + info.sw - 1
    657      else
    658        " align with variable
    659        let ind = indent(info.plnum) + info.sw
    660      endif
    661    endif
    662    return ind
    663  endif
    664 
    665  return -1
    666 endfunction
    667 
    668 function! s:PreviousNotMSL(msl_info) abort
    669  let info = a:msl_info
    670 
    671  " If the previous line wasn't a MSL
    672  if info.plnum != info.plnum_msl
    673    " If previous line ends bracket and begins non-bracket continuation decrease indent by 1.
    674    if s:Match(info.plnum, s:bracket_switch_continuation_regex)
    675      " TODO (2016-10-07) Wrong/unused? How could it be "1"?
    676      return indent(info.plnum) - 1
    677      " If previous line is a continuation return its indent.
    678    elseif s:Match(info.plnum, s:non_bracket_continuation_regex)
    679      return indent(info.plnum)
    680    endif
    681  endif
    682 
    683  return -1
    684 endfunction
    685 
    686 function! s:IndentingKeywordInMSL(msl_info) abort
    687  let info = a:msl_info
    688  " If the MSL line had an indenting keyword in it, add a level of indent.
    689  " TODO: this does not take into account contrived things such as
    690  " module Foo; class Bar; end
    691  let col = s:Match(info.plnum_msl, s:ruby_indent_keywords)
    692  if col > 0 && s:Match(info.plnum_msl, s:ruby_endless_def) <= 0
    693    let ind = indent(info.plnum_msl) + info.sw
    694    if s:Match(info.plnum_msl, s:end_end_regex)
    695      let ind = ind - info.sw
    696    elseif s:IsAssignment(getline(info.plnum_msl), col)
    697      if g:ruby_indent_assignment_style == 'hanging'
    698        " hanging indent
    699        let ind = col + info.sw - 1
    700      else
    701        " align with variable
    702        let ind = indent(info.plnum_msl) + info.sw
    703      endif
    704    endif
    705    return ind
    706  endif
    707  return -1
    708 endfunction
    709 
    710 function! s:ContinuedHangingOperator(msl_info) abort
    711  let info = a:msl_info
    712 
    713  " If the previous line ended with [*+/.,-=], but wasn't a block ending or a
    714  " closing bracket, indent one extra level.
    715  if s:Match(info.plnum_msl, s:non_bracket_continuation_regex) && !s:Match(info.plnum_msl, '^\s*\([\])}]\|end\)')
    716    if info.plnum_msl == info.plnum
    717      let ind = indent(info.plnum_msl) + info.sw
    718    else
    719      let ind = indent(info.plnum_msl)
    720    endif
    721    return ind
    722  endif
    723 
    724  return -1
    725 endfunction
    726 
    727 " 4. Auxiliary Functions {{{1
    728 " ======================
    729 
    730 function! s:IsInRubyGroup(groups, lnum, col) abort
    731  let ids = map(copy(a:groups), 'hlID("ruby".v:val)')
    732  return index(ids, synID(a:lnum, a:col, 1)) >= 0
    733 endfunction
    734 
    735 " Check if the character at lnum:col is inside a string, comment, or is ascii.
    736 function! s:IsInStringOrComment(lnum, col) abort
    737  return s:IsInRubyGroup(s:syng_strcom, a:lnum, a:col)
    738 endfunction
    739 
    740 " Check if the character at lnum:col is inside a string.
    741 function! s:IsInString(lnum, col) abort
    742  return s:IsInRubyGroup(s:syng_string, a:lnum, a:col)
    743 endfunction
    744 
    745 " Check if the character at lnum:col is inside a string or documentation.
    746 function! s:IsInStringOrDocumentation(lnum, col) abort
    747  return s:IsInRubyGroup(s:syng_stringdoc, a:lnum, a:col)
    748 endfunction
    749 
    750 " Check if the character at lnum:col is inside a string delimiter
    751 function! s:IsInStringDelimiter(lnum, col) abort
    752  return s:IsInRubyGroup(
    753        \ ['HeredocDelimiter', 'PercentStringDelimiter', 'StringDelimiter'],
    754        \ a:lnum, a:col
    755        \ )
    756 endfunction
    757 
    758 function! s:IsAssignment(str, pos) abort
    759  return strpart(a:str, 0, a:pos - 1) =~ '=\s*$'
    760 endfunction
    761 
    762 " Find line above 'lnum' that isn't empty, in a comment, or in a string.
    763 function! s:PrevNonBlankNonString(lnum) abort
    764  let in_block = 0
    765  let lnum = prevnonblank(a:lnum)
    766  while lnum > 0
    767    " Go in and out of blocks comments as necessary.
    768    " If the line isn't empty (with opt. comment) or in a string, end search.
    769    let line = getline(lnum)
    770    if line =~ '^=begin'
    771      if in_block
    772        let in_block = 0
    773      else
    774        break
    775      endif
    776    elseif !in_block && line =~ '^=end'
    777      let in_block = 1
    778    elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1)
    779          \ && s:IsInStringOrComment(lnum, strlen(line)))
    780      break
    781    endif
    782    let lnum = prevnonblank(lnum - 1)
    783  endwhile
    784  return lnum
    785 endfunction
    786 
    787 " Find line above 'lnum' that started the continuation 'lnum' may be part of.
    788 function! s:GetMSL(lnum) abort
    789  " Start on the line we're at and use its indent.
    790  let msl = a:lnum
    791  let lnum = s:PrevNonBlankNonString(a:lnum - 1)
    792  while lnum > 0
    793    " If we have a continuation line, or we're in a string, use line as MSL.
    794    " Otherwise, terminate search as we have found our MSL already.
    795    let line = getline(lnum)
    796 
    797    if !s:Match(msl, s:backslash_continuation_regex) &&
    798          \ s:Match(lnum, s:backslash_continuation_regex)
    799      " If the current line doesn't end in a backslash, but the previous one
    800      " does, look for that line's msl
    801      "
    802      " Example:
    803      "   foo = "bar" \
    804      "     "baz"
    805      "
    806      let msl = lnum
    807    elseif s:Match(msl, s:leading_operator_regex)
    808      " If the current line starts with a leading operator, keep its indent
    809      " and keep looking for an MSL.
    810      let msl = lnum
    811    elseif s:Match(lnum, s:splat_regex)
    812      " If the above line looks like the "*" of a splat, use the current one's
    813      " indentation.
    814      "
    815      " Example:
    816      "   Hash[*
    817      "     method_call do
    818      "       something
    819      "
    820      return msl
    821    elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
    822          \ s:Match(msl, s:non_bracket_continuation_regex)
    823      " If the current line is a non-bracket continuation and so is the
    824      " previous one, keep its indent and continue looking for an MSL.
    825      "
    826      " Example:
    827      "   method_call one,
    828      "     two,
    829      "     three
    830      "
    831      let msl = lnum
    832    elseif s:Match(lnum, s:dot_continuation_regex) &&
    833          \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
    834      " If the current line is a bracket continuation or a block-starter, but
    835      " the previous is a dot, keep going to see if the previous line is the
    836      " start of another continuation.
    837      "
    838      " Example:
    839      "   parent.
    840      "     method_call {
    841      "     three
    842      "
    843      let msl = lnum
    844    elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
    845          \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
    846      " If the current line is a bracket continuation or a block-starter, but
    847      " the previous is a non-bracket one, respect the previous' indentation,
    848      " and stop here.
    849      "
    850      " Example:
    851      "   method_call one,
    852      "     two {
    853      "     three
    854      "
    855      return lnum
    856    elseif s:Match(lnum, s:bracket_continuation_regex) &&
    857          \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
    858      " If both lines are bracket continuations (the current may also be a
    859      " block-starter), use the current one's and stop here
    860      "
    861      " Example:
    862      "   method_call(
    863      "     other_method_call(
    864      "       foo
    865      return msl
    866    elseif s:Match(lnum, s:block_regex) &&
    867          \ !s:Match(msl, s:continuation_regex) &&
    868          \ !s:Match(msl, s:block_continuation_regex)
    869      " If the previous line is a block-starter and the current one is
    870      " mostly ordinary, use the current one as the MSL.
    871      "
    872      " Example:
    873      "   method_call do
    874      "     something
    875      "     something_else
    876      return msl
    877    else
    878      let col = match(line, s:continuation_regex) + 1
    879      if (col > 0 && !s:IsInStringOrComment(lnum, col))
    880            \ || s:IsInString(lnum, strlen(line))
    881        let msl = lnum
    882      else
    883        break
    884      endif
    885    endif
    886 
    887    let lnum = s:PrevNonBlankNonString(lnum - 1)
    888  endwhile
    889  return msl
    890 endfunction
    891 
    892 " Check if line 'lnum' has more opening brackets than closing ones.
    893 function! s:ExtraBrackets(lnum) abort
    894  let opening = {'parentheses': [], 'braces': [], 'brackets': []}
    895  let closing = {'parentheses': [], 'braces': [], 'brackets': []}
    896 
    897  let line = getline(a:lnum)
    898  let pos  = match(line, '[][(){}]', 0)
    899 
    900  " Save any encountered opening brackets, and remove them once a matching
    901  " closing one has been found. If a closing bracket shows up that doesn't
    902  " close anything, save it for later.
    903  while pos != -1
    904    if !s:IsInStringOrComment(a:lnum, pos + 1)
    905      if line[pos] == '('
    906        call add(opening.parentheses, {'type': '(', 'pos': pos})
    907      elseif line[pos] == ')'
    908        if empty(opening.parentheses)
    909          call add(closing.parentheses, {'type': ')', 'pos': pos})
    910        else
    911          let opening.parentheses = opening.parentheses[0:-2]
    912        endif
    913      elseif line[pos] == '{'
    914        call add(opening.braces, {'type': '{', 'pos': pos})
    915      elseif line[pos] == '}'
    916        if empty(opening.braces)
    917          call add(closing.braces, {'type': '}', 'pos': pos})
    918        else
    919          let opening.braces = opening.braces[0:-2]
    920        endif
    921      elseif line[pos] == '['
    922        call add(opening.brackets, {'type': '[', 'pos': pos})
    923      elseif line[pos] == ']'
    924        if empty(opening.brackets)
    925          call add(closing.brackets, {'type': ']', 'pos': pos})
    926        else
    927          let opening.brackets = opening.brackets[0:-2]
    928        endif
    929      endif
    930    endif
    931 
    932    let pos = match(line, '[][(){}]', pos + 1)
    933  endwhile
    934 
    935  " Find the rightmost brackets, since they're the ones that are important in
    936  " both opening and closing cases
    937  let rightmost_opening = {'type': '(', 'pos': -1}
    938  let rightmost_closing = {'type': ')', 'pos': -1}
    939 
    940  for opening in opening.parentheses + opening.braces + opening.brackets
    941    if opening.pos > rightmost_opening.pos
    942      let rightmost_opening = opening
    943    endif
    944  endfor
    945 
    946  for closing in closing.parentheses + closing.braces + closing.brackets
    947    if closing.pos > rightmost_closing.pos
    948      let rightmost_closing = closing
    949    endif
    950  endfor
    951 
    952  return [rightmost_opening, rightmost_closing]
    953 endfunction
    954 
    955 function! s:Match(lnum, regex) abort
    956  let line   = getline(a:lnum)
    957  let offset = match(line, '\C'.a:regex)
    958  let col    = offset + 1
    959 
    960  while offset > -1 && s:IsInStringOrComment(a:lnum, col)
    961    let offset = match(line, '\C'.a:regex, offset + 1)
    962    let col = offset + 1
    963  endwhile
    964 
    965  if offset > -1
    966    return col
    967  else
    968    return 0
    969  endif
    970 endfunction
    971 
    972 " Locates the containing class/module's definition line, ignoring nested classes
    973 " along the way.
    974 "
    975 function! s:FindContainingClass() abort
    976  let saved_position = getpos('.')
    977 
    978  while searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
    979        \ s:end_skip_expr) > 0
    980    if expand('<cword>') =~# '\<class\|module\>'
    981      let found_lnum = line('.')
    982      call setpos('.', saved_position)
    983      return found_lnum
    984    endif
    985  endwhile
    986 
    987  call setpos('.', saved_position)
    988  return 0
    989 endfunction
    990 
    991 " }}}1
    992 
    993 let &cpo = s:cpo_save
    994 unlet s:cpo_save
    995 
    996 " vim:set sw=2 sts=2 ts=8 et: