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