blob: 2a267fdab36b8ebc8be0123f5265004a3100ac46 [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 Moolenaar4d8f4762021-06-27 15:18:56 +02007" Last Change: 2021 Feb 03
Bram Moolenaar60a795a2005-09-16 21:55:43 +00008
9" 0. Initialization {{{1
10" =================
Bram Moolenaar071d4272004-06-13 20:20:40 +000011
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
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020018if !exists('g:ruby_indent_access_modifier_style')
19 " Possible values: "normal", "indent", "outdent"
20 let g:ruby_indent_access_modifier_style = 'normal'
21endif
22
Bram Moolenaard09091d2019-01-17 16:07:22 +010023if !exists('g:ruby_indent_assignment_style')
24 " Possible values: "variable", "hanging"
25 let g:ruby_indent_assignment_style = 'hanging'
26endif
27
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020028if !exists('g:ruby_indent_block_style')
29 " Possible values: "expression", "do"
Bram Moolenaar942db232021-02-13 18:14:48 +010030 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
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020036endif
37
Bram Moolenaar551dbcc2006-04-25 22:13:59 +000038setlocal nosmartindent
39
Bram Moolenaar60a795a2005-09-16 21:55:43 +000040" Now, set up our indentation expression and keys that trigger it.
Bram Moolenaarec7944a2013-06-12 21:29:15 +020041setlocal indentexpr=GetRubyIndent(v:lnum)
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020042setlocal indentkeys=0{,0},0),0],!^F,o,O,e,:,.
Bram Moolenaarec7944a2013-06-12 21:29:15 +020043setlocal indentkeys+==end,=else,=elsif,=when,=ensure,=rescue,==begin,==end
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020044setlocal indentkeys+==private,=protected,=public
Bram Moolenaar071d4272004-06-13 20:20:40 +000045
46" Only define the function once.
47if exists("*GetRubyIndent")
48 finish
49endif
50
Bram Moolenaar60a795a2005-09-16 21:55:43 +000051let s:cpo_save = &cpo
52set cpo&vim
53
54" 1. Variables {{{1
55" ============
56
Bram Moolenaard09091d2019-01-17 16:07:22 +010057" Syntax group names that are strings.
Bram Moolenaar60a795a2005-09-16 21:55:43 +000058let s:syng_string =
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010059 \ ['String', 'Interpolation', 'InterpolationDelimiter', 'StringEscape']
Bram Moolenaar60a795a2005-09-16 21:55:43 +000060
Bram Moolenaard09091d2019-01-17 16:07:22 +010061" 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.
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010065let 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 \ ]
Bram Moolenaar60a795a2005-09-16 21:55:43 +000080
81" Expression used to check whether we should skip a match with searchpair().
82let s:skip_expr =
Bram Moolenaard09091d2019-01-17 16:07:22 +010083 \ 'index(map('.string(s:syng_strcom).',"hlID(''ruby''.v:val)"), synID(line("."),col("."),1)) >= 0'
Bram Moolenaar60a795a2005-09-16 21:55:43 +000084
85" Regex used for words that, at the start of a line, add a level of indent.
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020086let s:ruby_indent_keywords =
87 \ '^\s*\zs\<\%(module\|class\|if\|for' .
88 \ '\|while\|until\|else\|elsif\|case\|when\|unless\|begin\|ensure\|rescue' .
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010089 \ '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' .
Bram Moolenaarec7944a2013-06-12 21:29:15 +020090 \ '\|\%([=,*/%+-]\|<<\|>>\|:\s\)\s*\zs' .
91 \ '\<\%(if\|for\|while\|until\|case\|unless\|begin\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +000092
93" Regex used for words that, at the start of a line, remove a level of indent.
94let s:ruby_deindent_keywords =
Bram Moolenaarec7944a2013-06-12 21:29:15 +020095 \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|end\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +000096
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)?
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200100let s:end_start_regex =
101 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200102 \ '\<\%(module\|class\|if\|for\|while\|until\|case\|unless\|begin' .
Bram Moolenaar2ed639a2019-12-09 23:11:18 +0100103 \ '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' .
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200104 \ '\|\%(^\|[^.:@$]\)\@<=\<do:\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000105
106" Regex that defines the middle-match for the 'end' keyword.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200107let s:end_middle_regex = '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue:\@!\>\|when\|elsif\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000108
109" Regex that defines the end-match for the 'end' keyword.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200110let s:end_end_regex = '\%(^\|[^.:@$]\)\@<=\<end:\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000111
112" Expression used for searchpair() call for finding match for 'end' keyword.
113let s:end_skip_expr = s:skip_expr .
114 \ ' || (expand("<cword>") == "do"' .
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200115 \ ' && getline(".") =~ "^\\s*\\<\\(while\\|until\\|for\\):\\@!\\>")'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000116
117" Regex that defines continuation lines, not including (, {, or [.
Bram Moolenaar45758762016-10-12 23:08:06 +0200118let s:non_bracket_continuation_regex =
119 \ '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000120
121" Regex that defines continuation lines.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200122let s:continuation_regex =
Bram Moolenaar45758762016-10-12 23:08:06 +0200123 \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200124
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200125" Regex that defines continuable keywords
126let s:continuable_regex =
127 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
128 \ '\<\%(if\|for\|while\|until\|unless\):\@!\>'
129
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200130" Regex that defines bracket continuations
131let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$'
132
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200133" 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
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200142" Regex that defines the first part of a splat pattern
143let s:splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000144
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200145" 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
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000151" Regex that defines blocks.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200152"
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"
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000160let s:block_regex =
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200161 \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|[^|]*|\)\=\s*\%(#.*\)\=$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200162
163let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000164
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200165" Regex that describes a leading operator (only a method call's dot for now)
Bram Moolenaar2ed639a2019-12-09 23:11:18 +0100166let s:leading_operator_regex = '^\s*\%(&\=\.\)'
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200167
Bram Moolenaard09091d2019-01-17 16:07:22 +0100168" 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 " contination 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
Bram Moolenaar942db232021-02-13 18:14:48 +0100330 if g:ruby_indent_hanging_elements
331 let ind = virtcol('.') - 1
332 else
333 let ind = indent(line('.'))
334 end
Bram Moolenaard09091d2019-01-17 16:07:22 +0100335 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
Bram Moolenaar942db232021-02-13 18:14:48 +0100559 if !g:ruby_indent_hanging_elements
560 return indent(info.plnum) + info.sw
561 elseif opening.type == '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
Bram Moolenaard09091d2019-01-17 16:07:22 +0100562 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.
Bram Moolenaar942db232021-02-13 18:14:48 +0100646 elseif s:Match(info.plnum, s:non_bracket_continuation_regex)
Bram Moolenaard09091d2019-01-17 16:07:22 +0100647 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
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000696" ======================
697
Bram Moolenaard09091d2019-01-17 16:07:22 +0100698function! 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
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000703" Check if the character at lnum:col is inside a string, comment, or is ascii.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100704function! s:IsInStringOrComment(lnum, col) abort
705 return s:IsInRubyGroup(s:syng_strcom, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000706endfunction
707
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000708" Check if the character at lnum:col is inside a string.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100709function! s:IsInString(lnum, col) abort
710 return s:IsInRubyGroup(s:syng_string, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000711endfunction
712
713" Check if the character at lnum:col is inside a string or documentation.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100714function! s:IsInStringOrDocumentation(lnum, col) abort
715 return s:IsInRubyGroup(s:syng_stringdoc, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000716endfunction
717
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200718" Check if the character at lnum:col is inside a string delimiter
Bram Moolenaard09091d2019-01-17 16:07:22 +0100719function! s:IsInStringDelimiter(lnum, col) abort
Bram Moolenaar2ed639a2019-12-09 23:11:18 +0100720 return s:IsInRubyGroup(
721 \ ['HeredocDelimiter', 'PercentStringDelimiter', 'StringDelimiter'],
722 \ a:lnum, a:col
723 \ )
Bram Moolenaard09091d2019-01-17 16:07:22 +0100724endfunction
725
726function! s:IsAssignment(str, pos) abort
727 return strpart(a:str, 0, a:pos - 1) =~ '=\s*$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200728endfunction
729
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000730" Find line above 'lnum' that isn't empty, in a comment, or in a string.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100731function! s:PrevNonBlankNonString(lnum) abort
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000732 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)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200738 if line =~ '^=begin'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000739 if in_block
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200740 let in_block = 0
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000741 else
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200742 break
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000743 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200744 elseif !in_block && line =~ '^=end'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000745 let in_block = 1
746 elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200747 \ && s:IsInStringOrComment(lnum, strlen(line)))
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000748 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.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100756function! s:GetMSL(lnum) abort
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000757 " 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)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200764
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200765 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)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200780 " 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
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200789 elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200790 \ 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 "
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000799 let msl = lnum
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200800 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
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200812 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
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000845 else
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200846 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
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000853 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200854
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000855 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.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100861function! s:ExtraBrackets(lnum) abort
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200862 let opening = {'parentheses': [], 'braces': [], 'brackets': []}
863 let closing = {'parentheses': [], 'braces': [], 'brackets': []}
864
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000865 let line = getline(a:lnum)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200866 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.
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000871 while pos != -1
872 if !s:IsInStringOrComment(a:lnum, pos + 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200873 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
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000897 endif
898 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200899
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000900 let pos = match(line, '[][(){}]', pos + 1)
901 endwhile
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200902
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]
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000921endfunction
922
Bram Moolenaard09091d2019-01-17 16:07:22 +0100923function! s:Match(lnum, regex) abort
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200924 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
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000938endfunction
939
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200940" Locates the containing class/module's definition line, ignoring nested classes
941" along the way.
942"
Bram Moolenaard09091d2019-01-17 16:07:22 +0100943function! s:FindContainingClass() abort
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200944 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
Bram Moolenaar45758762016-10-12 23:08:06 +0200953 endwhile
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200954
955 call setpos('.', saved_position)
956 return 0
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000957endfunction
958
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000959" }}}1
960
961let &cpo = s:cpo_save
962unlet s:cpo_save
Bram Moolenaar9964e462007-05-05 17:54:07 +0000963
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200964" vim:set sw=2 sts=2 ts=8 et: