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