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