xref: /vim-8.2.3635/runtime/indent/ruby.vim (revision 818c9e7e)
1" Vim indent file
2" Language:		Ruby
3" Maintainer:		Nikolai Weibull <now at bitwi.se>
4" URL:			https://github.com/vim-ruby/vim-ruby
5" Release Coordinator:	Doug Kearns <[email protected]>
6
7" 0. Initialization {{{1
8" =================
9
10" Only load this indent file when no other was loaded.
11if exists("b:did_indent")
12  finish
13endif
14let b:did_indent = 1
15
16setlocal nosmartindent
17
18" Now, set up our indentation expression and keys that trigger it.
19setlocal indentexpr=GetRubyIndent(v:lnum)
20setlocal indentkeys=0{,0},0),0],!^F,o,O,e
21setlocal indentkeys+==end,=else,=elsif,=when,=ensure,=rescue,==begin,==end
22
23" Only define the function once.
24if exists("*GetRubyIndent")
25  finish
26endif
27
28let s:cpo_save = &cpo
29set cpo&vim
30
31" 1. Variables {{{1
32" ============
33
34" Regex of syntax group names that are or delimit strings/symbols or are comments.
35let s:syng_strcom = '\<ruby\%(Regexp\|RegexpDelimiter\|RegexpEscape' .
36      \ '\|Symbol\|String\|StringDelimiter\|StringEscape\|ASCIICode' .
37      \ '\|Interpolation\|NoInterpolation\|Comment\|Documentation\)\>'
38
39" Regex of syntax group names that are strings.
40let s:syng_string =
41      \ '\<ruby\%(String\|Interpolation\|NoInterpolation\|StringEscape\)\>'
42
43" Regex of syntax group names that are strings or documentation.
44let s:syng_stringdoc =
45      \'\<ruby\%(String\|Interpolation\|NoInterpolation\|StringEscape\|Documentation\)\>'
46
47" Expression used to check whether we should skip a match with searchpair().
48let s:skip_expr =
49      \ "synIDattr(synID(line('.'),col('.'),1),'name') =~ '".s:syng_strcom."'"
50
51" Regex used for words that, at the start of a line, add a level of indent.
52let s:ruby_indent_keywords = '^\s*\zs\<\%(module\|class\|def\|if\|for' .
53      \ '\|while\|until\|else\|elsif\|case\|when\|unless\|begin\|ensure' .
54      \ '\|rescue\):\@!\>' .
55      \ '\|\%([=,*/%+-]\|<<\|>>\|:\s\)\s*\zs' .
56      \    '\<\%(if\|for\|while\|until\|case\|unless\|begin\):\@!\>'
57
58" Regex used for words that, at the start of a line, remove a level of indent.
59let s:ruby_deindent_keywords =
60      \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|end\):\@!\>'
61
62" Regex that defines the start-match for the 'end' keyword.
63"let s:end_start_regex = '\%(^\|[^.]\)\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\|do\)\>'
64" TODO: the do here should be restricted somewhat (only at end of line)?
65let s:end_start_regex =
66      \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
67      \ '\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\):\@!\>' .
68      \ '\|\%(^\|[^.:@$]\)\@<=\<do:\@!\>'
69
70" Regex that defines the middle-match for the 'end' keyword.
71let s:end_middle_regex = '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue:\@!\>\|when\|elsif\):\@!\>'
72
73" Regex that defines the end-match for the 'end' keyword.
74let s:end_end_regex = '\%(^\|[^.:@$]\)\@<=\<end:\@!\>'
75
76" Expression used for searchpair() call for finding match for 'end' keyword.
77let s:end_skip_expr = s:skip_expr .
78      \ ' || (expand("<cword>") == "do"' .
79      \ ' && getline(".") =~ "^\\s*\\<\\(while\\|until\\|for\\):\\@!\\>")'
80
81" Regex that defines continuation lines, not including (, {, or [.
82let s:non_bracket_continuation_regex = '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$'
83
84" Regex that defines continuation lines.
85" TODO: this needs to deal with if ...: and so on
86let s:continuation_regex =
87      \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|\W[|&?]\|||\|&&\)\s*\%(#.*\)\=$'
88
89" Regex that defines bracket continuations
90let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$'
91
92" Regex that defines the first part of a splat pattern
93let s:splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$'
94
95" Regex that defines blocks.
96"
97" Note that there's a slight problem with this regex and s:continuation_regex.
98" Code like this will be matched by both:
99"
100"   method_call do |(a, b)|
101"
102" The reason is that the pipe matches a hanging "|" operator.
103"
104let s:block_regex =
105      \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|\s*(*\s*\%([*@&]\=\h\w*,\=\s*\)\%(,\s*(*\s*[*@&]\=\h\w*\s*)*\s*\)*|\)\=\s*\%(#.*\)\=$'
106
107let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex
108
109" 2. Auxiliary Functions {{{1
110" ======================
111
112" Check if the character at lnum:col is inside a string, comment, or is ascii.
113function s:IsInStringOrComment(lnum, col)
114  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_strcom
115endfunction
116
117" Check if the character at lnum:col is inside a string.
118function s:IsInString(lnum, col)
119  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_string
120endfunction
121
122" Check if the character at lnum:col is inside a string or documentation.
123function s:IsInStringOrDocumentation(lnum, col)
124  return synIDattr(synID(a:lnum, a:col, 1), 'name') =~ s:syng_stringdoc
125endfunction
126
127" Check if the character at lnum:col is inside a string delimiter
128function s:IsInStringDelimiter(lnum, col)
129  return synIDattr(synID(a:lnum, a:col, 1), 'name') == 'rubyStringDelimiter'
130endfunction
131
132" Find line above 'lnum' that isn't empty, in a comment, or in a string.
133function s:PrevNonBlankNonString(lnum)
134  let in_block = 0
135  let lnum = prevnonblank(a:lnum)
136  while lnum > 0
137    " Go in and out of blocks comments as necessary.
138    " If the line isn't empty (with opt. comment) or in a string, end search.
139    let line = getline(lnum)
140    if line =~ '^=begin'
141      if in_block
142        let in_block = 0
143      else
144        break
145      endif
146    elseif !in_block && line =~ '^=end'
147      let in_block = 1
148    elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1)
149          \ && s:IsInStringOrComment(lnum, strlen(line)))
150      break
151    endif
152    let lnum = prevnonblank(lnum - 1)
153  endwhile
154  return lnum
155endfunction
156
157" Find line above 'lnum' that started the continuation 'lnum' may be part of.
158function s:GetMSL(lnum)
159  " Start on the line we're at and use its indent.
160  let msl = a:lnum
161  let msl_body = getline(msl)
162  let lnum = s:PrevNonBlankNonString(a:lnum - 1)
163  while lnum > 0
164    " If we have a continuation line, or we're in a string, use line as MSL.
165    " Otherwise, terminate search as we have found our MSL already.
166    let line = getline(lnum)
167
168    if s:Match(lnum, s:splat_regex)
169      " If the above line looks like the "*" of a splat, use the current one's
170      " indentation.
171      "
172      " Example:
173      "   Hash[*
174      "     method_call do
175      "       something
176      "
177      return msl
178    elseif s:Match(line, s:non_bracket_continuation_regex) &&
179          \ s:Match(msl, s:non_bracket_continuation_regex)
180      " If the current line is a non-bracket continuation and so is the
181      " previous one, keep its indent and continue looking for an MSL.
182      "
183      " Example:
184      "   method_call one,
185      "     two,
186      "     three
187      "
188      let msl = lnum
189    elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
190          \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
191      " If the current line is a bracket continuation or a block-starter, but
192      " the previous is a non-bracket one, respect the previous' indentation,
193      " and stop here.
194      "
195      " Example:
196      "   method_call one,
197      "     two {
198      "     three
199      "
200      return lnum
201    elseif s:Match(lnum, s:bracket_continuation_regex) &&
202          \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
203      " If both lines are bracket continuations (the current may also be a
204      " block-starter), use the current one's and stop here
205      "
206      " Example:
207      "   method_call(
208      "     other_method_call(
209      "       foo
210      return msl
211    elseif s:Match(lnum, s:block_regex) &&
212          \ !s:Match(msl, s:continuation_regex) &&
213          \ !s:Match(msl, s:block_continuation_regex)
214      " If the previous line is a block-starter and the current one is
215      " mostly ordinary, use the current one as the MSL.
216      "
217      " Example:
218      "   method_call do
219      "     something
220      "     something_else
221      return msl
222    else
223      let col = match(line, s:continuation_regex) + 1
224      if (col > 0 && !s:IsInStringOrComment(lnum, col))
225            \ || s:IsInString(lnum, strlen(line))
226        let msl = lnum
227      else
228        break
229      endif
230    endif
231
232    let msl_body = getline(msl)
233    let lnum = s:PrevNonBlankNonString(lnum - 1)
234  endwhile
235  return msl
236endfunction
237
238" Check if line 'lnum' has more opening brackets than closing ones.
239function s:ExtraBrackets(lnum)
240  let opening = {'parentheses': [], 'braces': [], 'brackets': []}
241  let closing = {'parentheses': [], 'braces': [], 'brackets': []}
242
243  let line = getline(a:lnum)
244  let pos  = match(line, '[][(){}]', 0)
245
246  " Save any encountered opening brackets, and remove them once a matching
247  " closing one has been found. If a closing bracket shows up that doesn't
248  " close anything, save it for later.
249  while pos != -1
250    if !s:IsInStringOrComment(a:lnum, pos + 1)
251      if line[pos] == '('
252        call add(opening.parentheses, {'type': '(', 'pos': pos})
253      elseif line[pos] == ')'
254        if empty(opening.parentheses)
255          call add(closing.parentheses, {'type': ')', 'pos': pos})
256        else
257          let opening.parentheses = opening.parentheses[0:-2]
258        endif
259      elseif line[pos] == '{'
260        call add(opening.braces, {'type': '{', 'pos': pos})
261      elseif line[pos] == '}'
262        if empty(opening.braces)
263          call add(closing.braces, {'type': '}', 'pos': pos})
264        else
265          let opening.braces = opening.braces[0:-2]
266        endif
267      elseif line[pos] == '['
268        call add(opening.brackets, {'type': '[', 'pos': pos})
269      elseif line[pos] == ']'
270        if empty(opening.brackets)
271          call add(closing.brackets, {'type': ']', 'pos': pos})
272        else
273          let opening.brackets = opening.brackets[0:-2]
274        endif
275      endif
276    endif
277
278    let pos = match(line, '[][(){}]', pos + 1)
279  endwhile
280
281  " Find the rightmost brackets, since they're the ones that are important in
282  " both opening and closing cases
283  let rightmost_opening = {'type': '(', 'pos': -1}
284  let rightmost_closing = {'type': ')', 'pos': -1}
285
286  for opening in opening.parentheses + opening.braces + opening.brackets
287    if opening.pos > rightmost_opening.pos
288      let rightmost_opening = opening
289    endif
290  endfor
291
292  for closing in closing.parentheses + closing.braces + closing.brackets
293    if closing.pos > rightmost_closing.pos
294      let rightmost_closing = closing
295    endif
296  endfor
297
298  return [rightmost_opening, rightmost_closing]
299endfunction
300
301function s:Match(lnum, regex)
302  let col = match(getline(a:lnum), '\C'.a:regex) + 1
303  return col > 0 && !s:IsInStringOrComment(a:lnum, col) ? col : 0
304endfunction
305
306function s:MatchLast(lnum, regex)
307  let line = getline(a:lnum)
308  let col = match(line, '.*\zs' . a:regex)
309  while col != -1 && s:IsInStringOrComment(a:lnum, col)
310    let line = strpart(line, 0, col)
311    let col = match(line, '.*' . a:regex)
312  endwhile
313  return col + 1
314endfunction
315
316" 3. GetRubyIndent Function {{{1
317" =========================
318
319function GetRubyIndent(...)
320  " 3.1. Setup {{{2
321  " ----------
322
323  " For the current line, use the first argument if given, else v:lnum
324  let clnum = a:0 ? a:1 : v:lnum
325
326  " Set up variables for restoring position in file.  Could use clnum here.
327  let vcol = col('.')
328
329  " 3.2. Work on the current line {{{2
330  " -----------------------------
331
332  " Get the current line.
333  let line = getline(clnum)
334  let ind = -1
335
336  " If we got a closing bracket on an empty line, find its match and indent
337  " according to it.  For parentheses we indent to its column - 1, for the
338  " others we indent to the containing line's MSL's level.  Return -1 if fail.
339  let col = matchend(line, '^\s*[]})]')
340  if col > 0 && !s:IsInStringOrComment(clnum, col)
341    call cursor(clnum, col)
342    let bs = strpart('(){}[]', stridx(')}]', line[col - 1]) * 2, 2)
343    if searchpair(escape(bs[0], '\['), '', bs[1], 'bW', s:skip_expr) > 0
344      if line[col-1]==')' && col('.') != col('$') - 1
345        let ind = virtcol('.') - 1
346      else
347        let ind = indent(s:GetMSL(line('.')))
348      endif
349    endif
350    return ind
351  endif
352
353  " If we have a =begin or =end set indent to first column.
354  if match(line, '^\s*\%(=begin\|=end\)$') != -1
355    return 0
356  endif
357
358  " If we have a deindenting keyword, find its match and indent to its level.
359  " TODO: this is messy
360  if s:Match(clnum, s:ruby_deindent_keywords)
361    call cursor(clnum, 1)
362    if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
363          \ s:end_skip_expr) > 0
364      let msl  = s:GetMSL(line('.'))
365      let line = getline(line('.'))
366
367      if strpart(line, 0, col('.') - 1) =~ '=\s*$' &&
368            \ strpart(line, col('.') - 1, 2) !~ 'do'
369        let ind = virtcol('.') - 1
370      elseif getline(msl) =~ '=\s*\(#.*\)\=$'
371        let ind = indent(line('.'))
372      else
373        let ind = indent(msl)
374      endif
375    endif
376    return ind
377  endif
378
379  " If we are in a multi-line string or line-comment, don't do anything to it.
380  if s:IsInStringOrDocumentation(clnum, matchend(line, '^\s*') + 1)
381    return indent('.')
382  endif
383
384  " If we are at the closing delimiter of a "<<" heredoc-style string, set the
385  " indent to 0.
386  if line =~ '^\k\+\s*$'
387        \ && s:IsInStringDelimiter(clnum, 1)
388        \ && search('\V<<'.line, 'nbW') > 0
389    return 0
390  endif
391
392  " 3.3. Work on the previous line. {{{2
393  " -------------------------------
394
395  " Find a non-blank, non-multi-line string line above the current line.
396  let lnum = s:PrevNonBlankNonString(clnum - 1)
397
398  " If the line is empty and inside a string, use the previous line.
399  if line =~ '^\s*$' && lnum != prevnonblank(clnum - 1)
400    return indent(prevnonblank(clnum))
401  endif
402
403  " At the start of the file use zero indent.
404  if lnum == 0
405    return 0
406  endif
407
408  " Set up variables for the previous line.
409  let line = getline(lnum)
410  let ind = indent(lnum)
411
412  " If the previous line ended with a block opening, add a level of indent.
413  if s:Match(lnum, s:block_regex)
414    return indent(s:GetMSL(lnum)) + &sw
415  endif
416
417  " If the previous line ended with the "*" of a splat, add a level of indent
418  if line =~ s:splat_regex
419    return indent(lnum) + &sw
420  endif
421
422  " If the previous line contained unclosed opening brackets and we are still
423  " in them, find the rightmost one and add indent depending on the bracket
424  " type.
425  "
426  " If it contained hanging closing brackets, find the rightmost one, find its
427  " match and indent according to that.
428  if line =~ '[[({]' || line =~ '[])}]\s*\%(#.*\)\=$'
429    let [opening, closing] = s:ExtraBrackets(lnum)
430
431    if opening.pos != -1
432      if opening.type == '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
433        if col('.') + 1 == col('$')
434          return ind + &sw
435        else
436          return virtcol('.')
437        endif
438      else
439        let nonspace = matchend(line, '\S', opening.pos + 1) - 1
440        return nonspace > 0 ? nonspace : ind + &sw
441      endif
442    elseif closing.pos != -1
443      call cursor(lnum, closing.pos + 1)
444      normal! %
445
446      if s:Match(line('.'), s:ruby_indent_keywords)
447        return indent('.') + &sw
448      else
449        return indent('.')
450      endif
451    else
452      call cursor(clnum, vcol)
453    end
454  endif
455
456  " If the previous line ended with an "end", match that "end"s beginning's
457  " indent.
458  let col = s:Match(lnum, '\%(^\|[^.:@$]\)\<end\>\s*\%(#.*\)\=$')
459  if col > 0
460    call cursor(lnum, col)
461    if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW',
462          \ s:end_skip_expr) > 0
463      let n = line('.')
464      let ind = indent('.')
465      let msl = s:GetMSL(n)
466      if msl != n
467        let ind = indent(msl)
468      end
469      return ind
470    endif
471  end
472
473  let col = s:Match(lnum, s:ruby_indent_keywords)
474  if col > 0
475    call cursor(lnum, col)
476    let ind = virtcol('.') - 1 + &sw
477    " TODO: make this better (we need to count them) (or, if a searchpair
478    " fails, we know that something is lacking an end and thus we indent a
479    " level
480    if s:Match(lnum, s:end_end_regex)
481      let ind = indent('.')
482    endif
483    return ind
484  endif
485
486  " 3.4. Work on the MSL line. {{{2
487  " --------------------------
488
489  " Set up variables to use and search for MSL to the previous line.
490  let p_lnum = lnum
491  let lnum = s:GetMSL(lnum)
492
493  " If the previous line wasn't a MSL and is continuation return its indent.
494  " TODO: the || s:IsInString() thing worries me a bit.
495  if p_lnum != lnum
496    if s:Match(p_lnum, s:non_bracket_continuation_regex) || s:IsInString(p_lnum,strlen(line))
497      return ind
498    endif
499  endif
500
501  " Set up more variables, now that we know we wasn't continuation bound.
502  let line = getline(lnum)
503  let msl_ind = indent(lnum)
504
505  " If the MSL line had an indenting keyword in it, add a level of indent.
506  " TODO: this does not take into account contrived things such as
507  " module Foo; class Bar; end
508  if s:Match(lnum, s:ruby_indent_keywords)
509    let ind = msl_ind + &sw
510    if s:Match(lnum, s:end_end_regex)
511      let ind = ind - &sw
512    endif
513    return ind
514  endif
515
516  " If the previous line ended with [*+/.,-=], but wasn't a block ending or a
517  " closing bracket, indent one extra level.
518  if s:Match(lnum, s:non_bracket_continuation_regex) && !s:Match(lnum, '^\s*\([\])}]\|end\)')
519    if lnum == p_lnum
520      let ind = msl_ind + &sw
521    else
522      let ind = msl_ind
523    endif
524    return ind
525  endif
526
527  " }}}2
528
529  return ind
530endfunction
531
532" }}}1
533
534let &cpo = s:cpo_save
535unlet s:cpo_save
536
537" vim:set sw=2 sts=2 ts=8 et:
538