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