blob: ea5a2a749419ed850c562af673b68a7350844735 [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>
Doug Kearnsda16a1b2023-09-01 18:33:33 +02007" Last Change: 2022 Jun 30
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 Moolenaar46eea442022-03-30 10:51:39 +010043setlocal indentkeys+==end,=else,=elsif,=when,=in\ ,=ensure,=rescue,==begin,==end
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020044setlocal indentkeys+==private,=protected,=public
Bram Moolenaar071d4272004-06-13 20:20:40 +000045
Bram Moolenaar46eea442022-03-30 10:51:39 +010046let b:undo_indent = "setlocal indentexpr< indentkeys< smartindent<"
47
Bram Moolenaar071d4272004-06-13 20:20:40 +000048" Only define the function once.
49if exists("*GetRubyIndent")
50 finish
51endif
52
Bram Moolenaar60a795a2005-09-16 21:55:43 +000053let s:cpo_save = &cpo
54set cpo&vim
55
56" 1. Variables {{{1
57" ============
58
Bram Moolenaard09091d2019-01-17 16:07:22 +010059" Syntax group names that are strings.
Bram Moolenaar60a795a2005-09-16 21:55:43 +000060let s:syng_string =
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010061 \ ['String', 'Interpolation', 'InterpolationDelimiter', 'StringEscape']
Bram Moolenaar60a795a2005-09-16 21:55:43 +000062
Bram Moolenaard09091d2019-01-17 16:07:22 +010063" Syntax group names that are strings or documentation.
64let s:syng_stringdoc = s:syng_string + ['Documentation']
65
66" Syntax group names that are or delimit strings/symbols/regexes or are comments.
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010067let s:syng_strcom = s:syng_stringdoc + [
68 \ 'Character',
69 \ 'Comment',
70 \ 'HeredocDelimiter',
71 \ 'PercentRegexpDelimiter',
72 \ 'PercentStringDelimiter',
73 \ 'PercentSymbolDelimiter',
74 \ 'Regexp',
75 \ 'RegexpCharClass',
76 \ 'RegexpDelimiter',
77 \ 'RegexpEscape',
78 \ 'StringDelimiter',
79 \ 'Symbol',
80 \ 'SymbolDelimiter',
81 \ ]
Bram Moolenaar60a795a2005-09-16 21:55:43 +000082
83" Expression used to check whether we should skip a match with searchpair().
84let s:skip_expr =
Bram Moolenaard09091d2019-01-17 16:07:22 +010085 \ 'index(map('.string(s:syng_strcom).',"hlID(''ruby''.v:val)"), synID(line("."),col("."),1)) >= 0'
Bram Moolenaar60a795a2005-09-16 21:55:43 +000086
87" Regex used for words that, at the start of a line, add a level of indent.
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020088let s:ruby_indent_keywords =
89 \ '^\s*\zs\<\%(module\|class\|if\|for' .
Bram Moolenaar46eea442022-03-30 10:51:39 +010090 \ '\|while\|until\|else\|elsif\|case\|when\|in\|unless\|begin\|ensure\|rescue' .
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010091 \ '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' .
Bram Moolenaarec7944a2013-06-12 21:29:15 +020092 \ '\|\%([=,*/%+-]\|<<\|>>\|:\s\)\s*\zs' .
93 \ '\<\%(if\|for\|while\|until\|case\|unless\|begin\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +000094
Bram Moolenaar46eea442022-03-30 10:51:39 +010095" Def without an end clause: def method_call(...) = <expression>
Doug Kearnsda16a1b2023-09-01 18:33:33 +020096let s:ruby_endless_def = '\<def\s\+\%(\k\+\.\)\=\k\+[!?]\=\%((.*)\|\s\)\s*='
Bram Moolenaar46eea442022-03-30 10:51:39 +010097
Bram Moolenaar60a795a2005-09-16 21:55:43 +000098" Regex used for words that, at the start of a line, remove a level of indent.
99let s:ruby_deindent_keywords =
Bram Moolenaar46eea442022-03-30 10:51:39 +0100100 \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|in\|end\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000101
102" Regex that defines the start-match for the 'end' keyword.
103"let s:end_start_regex = '\%(^\|[^.]\)\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\|do\)\>'
104" TODO: the do here should be restricted somewhat (only at end of line)?
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200105let s:end_start_regex =
106 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200107 \ '\<\%(module\|class\|if\|for\|while\|until\|case\|unless\|begin' .
Bram Moolenaar2ed639a2019-12-09 23:11:18 +0100108 \ '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' .
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200109 \ '\|\%(^\|[^.:@$]\)\@<=\<do:\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000110
111" Regex that defines the middle-match for the 'end' keyword.
Bram Moolenaar46eea442022-03-30 10:51:39 +0100112let s:end_middle_regex = '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue:\@!\>\|when\|\%(\%(^\|;\)\s*\)\@<=\<in\|elsif\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000113
114" Regex that defines the end-match for the 'end' keyword.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200115let s:end_end_regex = '\%(^\|[^.:@$]\)\@<=\<end:\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000116
Bram Moolenaar46eea442022-03-30 10:51:39 +0100117" Expression used for searchpair() call for finding a match for an 'end' keyword.
118function! s:EndSkipExpr()
119 if eval(s:skip_expr)
120 return 1
121 elseif expand('<cword>') == 'do'
122 \ && getline(".") =~ '^\s*\<\(while\|until\|for\):\@!\>'
123 return 1
124 elseif getline('.') =~ s:ruby_endless_def
125 return 1
126 elseif getline('.') =~ '\<def\s\+\k\+[!?]\=([^)]*$'
127 " Then it's a `def method(` with a possible `) =` later
128 call search('\<def\s\+\k\+\zs(', 'W', line('.'))
129 normal! %
130 return getline('.') =~ ')\s*='
131 else
132 return 0
133 endif
134endfunction
135
136let s:end_skip_expr = function('s:EndSkipExpr')
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000137
138" Regex that defines continuation lines, not including (, {, or [.
Bram Moolenaar45758762016-10-12 23:08:06 +0200139let s:non_bracket_continuation_regex =
140 \ '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000141
142" Regex that defines continuation lines.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200143let s:continuation_regex =
Bram Moolenaar45758762016-10-12 23:08:06 +0200144 \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200145
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200146" Regex that defines continuable keywords
147let s:continuable_regex =
148 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
149 \ '\<\%(if\|for\|while\|until\|unless\):\@!\>'
150
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200151" Regex that defines bracket continuations
152let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$'
153
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200154" Regex that defines dot continuations
155let s:dot_continuation_regex = '%\@<!\.\s*\%(#.*\)\=$'
156
157" Regex that defines backslash continuations
158let s:backslash_continuation_regex = '%\@<!\\\s*$'
159
160" Regex that defines end of bracket continuation followed by another continuation
161let s:bracket_switch_continuation_regex = '^\([^(]\+\zs).\+\)\+'.s:continuation_regex
162
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200163" Regex that defines the first part of a splat pattern
164let s:splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000165
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200166" Regex that describes all indent access modifiers
167let s:access_modifier_regex = '\C^\s*\%(public\|protected\|private\)\s*\%(#.*\)\=$'
168
169" Regex that describes the indent access modifiers (excludes public)
170let s:indent_access_modifier_regex = '\C^\s*\%(protected\|private\)\s*\%(#.*\)\=$'
171
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000172" Regex that defines blocks.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200173"
174" Note that there's a slight problem with this regex and s:continuation_regex.
175" Code like this will be matched by both:
176"
177" method_call do |(a, b)|
178"
179" The reason is that the pipe matches a hanging "|" operator.
180"
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000181let s:block_regex =
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200182 \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|[^|]*|\)\=\s*\%(#.*\)\=$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200183
184let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000185
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200186" Regex that describes a leading operator (only a method call's dot for now)
Bram Moolenaar2ed639a2019-12-09 23:11:18 +0100187let s:leading_operator_regex = '^\s*\%(&\=\.\)'
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200188
Bram Moolenaard09091d2019-01-17 16:07:22 +0100189" 2. GetRubyIndent Function {{{1
190" =========================
191
192function! GetRubyIndent(...) abort
193 " 2.1. Setup {{{2
194 " ----------
195
196 let indent_info = {}
197
198 " The value of a single shift-width
199 if exists('*shiftwidth')
200 let indent_info.sw = shiftwidth()
201 else
202 let indent_info.sw = &sw
203 endif
204
205 " For the current line, use the first argument if given, else v:lnum
206 let indent_info.clnum = a:0 ? a:1 : v:lnum
207 let indent_info.cline = getline(indent_info.clnum)
208
209 " Set up variables for restoring position in file. Could use clnum here.
210 let indent_info.col = col('.')
211
212 " 2.2. Work on the current line {{{2
213 " -----------------------------
214 let indent_callback_names = [
215 \ 's:AccessModifier',
216 \ 's:ClosingBracketOnEmptyLine',
217 \ 's:BlockComment',
218 \ 's:DeindentingKeyword',
219 \ 's:MultilineStringOrLineComment',
220 \ 's:ClosingHeredocDelimiter',
221 \ 's:LeadingOperator',
222 \ ]
223
224 for callback_name in indent_callback_names
225" Decho "Running: ".callback_name
226 let indent = call(function(callback_name), [indent_info])
227
228 if indent >= 0
229" Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
230 return indent
231 endif
232 endfor
233
234 " 2.3. Work on the previous line. {{{2
235 " -------------------------------
236
237 " Special case: we don't need the real s:PrevNonBlankNonString for an empty
238 " line inside a string. And that call can be quite expensive in that
239 " particular situation.
240 let indent_callback_names = [
241 \ 's:EmptyInsideString',
242 \ ]
243
244 for callback_name in indent_callback_names
245" Decho "Running: ".callback_name
246 let indent = call(function(callback_name), [indent_info])
247
248 if indent >= 0
249" Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
250 return indent
251 endif
252 endfor
253
254 " Previous line number
255 let indent_info.plnum = s:PrevNonBlankNonString(indent_info.clnum - 1)
256 let indent_info.pline = getline(indent_info.plnum)
257
258 let indent_callback_names = [
259 \ 's:StartOfFile',
260 \ 's:AfterAccessModifier',
261 \ 's:ContinuedLine',
262 \ 's:AfterBlockOpening',
263 \ 's:AfterHangingSplat',
264 \ 's:AfterUnbalancedBracket',
265 \ 's:AfterLeadingOperator',
266 \ 's:AfterEndKeyword',
267 \ 's:AfterIndentKeyword',
268 \ ]
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.4. Work on the MSL line. {{{2
281 " --------------------------
282 let indent_callback_names = [
283 \ 's:PreviousNotMSL',
284 \ 's:IndentingKeywordInMSL',
285 \ 's:ContinuedHangingOperator',
286 \ ]
287
288 " Most Significant line based on the previous one -- in case it's a
Bram Moolenaar6c391a72021-09-09 21:55:11 +0200289 " continuation of something above
Bram Moolenaard09091d2019-01-17 16:07:22 +0100290 let indent_info.plnum_msl = s:GetMSL(indent_info.plnum)
291
292 for callback_name in indent_callback_names
293" Decho "Running: ".callback_name
294 let indent = call(function(callback_name), [indent_info])
295
296 if indent >= 0
297" Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
298 return indent
299 endif
300 endfor
301
302 " }}}2
303
304 " By default, just return the previous line's indent
305" Decho "Default case matched"
306 return indent(indent_info.plnum)
307endfunction
308
309" 3. Indenting Logic Callbacks {{{1
310" ============================
311
312function! s:AccessModifier(cline_info) abort
313 let info = a:cline_info
314
315 " If this line is an access modifier keyword, align according to the closest
316 " class declaration.
317 if g:ruby_indent_access_modifier_style == 'indent'
318 if s:Match(info.clnum, s:access_modifier_regex)
319 let class_lnum = s:FindContainingClass()
320 if class_lnum > 0
321 return indent(class_lnum) + info.sw
322 endif
323 endif
324 elseif g:ruby_indent_access_modifier_style == 'outdent'
325 if s:Match(info.clnum, s:access_modifier_regex)
326 let class_lnum = s:FindContainingClass()
327 if class_lnum > 0
328 return indent(class_lnum)
329 endif
330 endif
331 endif
332
333 return -1
334endfunction
335
336function! s:ClosingBracketOnEmptyLine(cline_info) abort
337 let info = a:cline_info
338
339 " If we got a closing bracket on an empty line, find its match and indent
340 " according to it. For parentheses we indent to its column - 1, for the
341 " others we indent to the containing line's MSL's level. Return -1 if fail.
342 let col = matchend(info.cline, '^\s*[]})]')
343
344 if col > 0 && !s:IsInStringOrComment(info.clnum, col)
345 call cursor(info.clnum, col)
346 let closing_bracket = info.cline[col - 1]
347 let bracket_pair = strpart('(){}[]', stridx(')}]', closing_bracket) * 2, 2)
348
349 if searchpair(escape(bracket_pair[0], '\['), '', bracket_pair[1], 'bW', s:skip_expr) > 0
350 if closing_bracket == ')' && col('.') != col('$') - 1
Bram Moolenaar942db232021-02-13 18:14:48 +0100351 if g:ruby_indent_hanging_elements
352 let ind = virtcol('.') - 1
353 else
354 let ind = indent(line('.'))
355 end
Bram Moolenaard09091d2019-01-17 16:07:22 +0100356 elseif g:ruby_indent_block_style == 'do'
357 let ind = indent(line('.'))
358 else " g:ruby_indent_block_style == 'expression'
359 let ind = indent(s:GetMSL(line('.')))
360 endif
361 endif
362
363 return ind
364 endif
365
366 return -1
367endfunction
368
369function! s:BlockComment(cline_info) abort
370 " If we have a =begin or =end set indent to first column.
371 if match(a:cline_info.cline, '^\s*\%(=begin\|=end\)$') != -1
372 return 0
373 endif
374 return -1
375endfunction
376
377function! s:DeindentingKeyword(cline_info) abort
378 let info = a:cline_info
379
380 " If we have a deindenting keyword, find its match and indent to its level.
381 " TODO: this is messy
382 if s:Match(info.clnum, s:ruby_deindent_keywords)
383 call cursor(info.clnum, 1)
384
385 if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
386 \ s:end_skip_expr) > 0
387 let msl = s:GetMSL(line('.'))
388 let line = getline(line('.'))
389
390 if s:IsAssignment(line, col('.')) &&
391 \ strpart(line, col('.') - 1, 2) !~ 'do'
392 " assignment to case/begin/etc, on the same line
393 if g:ruby_indent_assignment_style == 'hanging'
394 " hanging indent
395 let ind = virtcol('.') - 1
396 else
397 " align with variable
398 let ind = indent(line('.'))
399 endif
400 elseif g:ruby_indent_block_style == 'do'
401 " align to line of the "do", not to the MSL
402 let ind = indent(line('.'))
403 elseif getline(msl) =~ '=\s*\(#.*\)\=$'
404 " in the case of assignment to the MSL, align to the starting line,
405 " not to the MSL
406 let ind = indent(line('.'))
407 else
408 " align to the MSL
409 let ind = indent(msl)
410 endif
411 endif
412 return ind
413 endif
414
415 return -1
416endfunction
417
418function! s:MultilineStringOrLineComment(cline_info) abort
419 let info = a:cline_info
420
421 " If we are in a multi-line string or line-comment, don't do anything to it.
422 if s:IsInStringOrDocumentation(info.clnum, matchend(info.cline, '^\s*') + 1)
423 return indent(info.clnum)
424 endif
425 return -1
426endfunction
427
428function! s:ClosingHeredocDelimiter(cline_info) abort
429 let info = a:cline_info
430
431 " If we are at the closing delimiter of a "<<" heredoc-style string, set the
432 " indent to 0.
433 if info.cline =~ '^\k\+\s*$'
434 \ && s:IsInStringDelimiter(info.clnum, 1)
435 \ && search('\V<<'.info.cline, 'nbW') > 0
436 return 0
437 endif
438
439 return -1
440endfunction
441
442function! s:LeadingOperator(cline_info) abort
443 " If the current line starts with a leading operator, add a level of indent.
444 if s:Match(a:cline_info.clnum, s:leading_operator_regex)
445 return indent(s:GetMSL(a:cline_info.clnum)) + a:cline_info.sw
446 endif
447 return -1
448endfunction
449
450function! s:EmptyInsideString(pline_info) abort
451 " If the line is empty and inside a string (the previous line is a string,
452 " too), use the previous line's indent
453 let info = a:pline_info
454
455 let plnum = prevnonblank(info.clnum - 1)
456 let pline = getline(plnum)
457
458 if info.cline =~ '^\s*$'
459 \ && s:IsInStringOrComment(plnum, 1)
460 \ && s:IsInStringOrComment(plnum, strlen(pline))
461 return indent(plnum)
462 endif
463 return -1
464endfunction
465
466function! s:StartOfFile(pline_info) abort
467 " At the start of the file use zero indent.
468 if a:pline_info.plnum == 0
469 return 0
470 endif
471 return -1
472endfunction
473
474function! s:AfterAccessModifier(pline_info) abort
475 let info = a:pline_info
476
477 if g:ruby_indent_access_modifier_style == 'indent'
478 " If the previous line was a private/protected keyword, add a
479 " level of indent.
480 if s:Match(info.plnum, s:indent_access_modifier_regex)
481 return indent(info.plnum) + info.sw
482 endif
483 elseif g:ruby_indent_access_modifier_style == 'outdent'
484 " If the previous line was a private/protected/public keyword, add
485 " a level of indent, since the keyword has been out-dented.
486 if s:Match(info.plnum, s:access_modifier_regex)
487 return indent(info.plnum) + info.sw
488 endif
489 endif
490 return -1
491endfunction
492
493" Example:
494"
495" if foo || bar ||
496" baz || bing
497" puts "foo"
498" end
499"
500function! s:ContinuedLine(pline_info) abort
501 let info = a:pline_info
502
503 let col = s:Match(info.plnum, s:ruby_indent_keywords)
504 if s:Match(info.plnum, s:continuable_regex) &&
505 \ s:Match(info.plnum, s:continuation_regex)
506 if col > 0 && s:IsAssignment(info.pline, col)
507 if g:ruby_indent_assignment_style == 'hanging'
508 " hanging indent
509 let ind = col - 1
510 else
511 " align with variable
512 let ind = indent(info.plnum)
513 endif
514 else
515 let ind = indent(s:GetMSL(info.plnum))
516 endif
517 return ind + info.sw + info.sw
518 endif
519 return -1
520endfunction
521
522function! s:AfterBlockOpening(pline_info) abort
523 let info = a:pline_info
524
525 " If the previous line ended with a block opening, add a level of indent.
526 if s:Match(info.plnum, s:block_regex)
527 if g:ruby_indent_block_style == 'do'
528 " don't align to the msl, align to the "do"
529 let ind = indent(info.plnum) + info.sw
530 else
531 let plnum_msl = s:GetMSL(info.plnum)
532
533 if getline(plnum_msl) =~ '=\s*\(#.*\)\=$'
534 " in the case of assignment to the msl, align to the starting line,
535 " not to the msl
536 let ind = indent(info.plnum) + info.sw
537 else
538 let ind = indent(plnum_msl) + info.sw
539 endif
540 endif
541
542 return ind
543 endif
544
545 return -1
546endfunction
547
548function! s:AfterLeadingOperator(pline_info) abort
549 " If the previous line started with a leading operator, use its MSL's level
550 " of indent
551 if s:Match(a:pline_info.plnum, s:leading_operator_regex)
552 return indent(s:GetMSL(a:pline_info.plnum))
553 endif
554 return -1
555endfunction
556
557function! s:AfterHangingSplat(pline_info) abort
558 let info = a:pline_info
559
560 " If the previous line ended with the "*" of a splat, add a level of indent
561 if info.pline =~ s:splat_regex
562 return indent(info.plnum) + info.sw
563 endif
564 return -1
565endfunction
566
567function! s:AfterUnbalancedBracket(pline_info) abort
568 let info = a:pline_info
569
570 " If the previous line contained unclosed opening brackets and we are still
571 " in them, find the rightmost one and add indent depending on the bracket
572 " type.
573 "
574 " If it contained hanging closing brackets, find the rightmost one, find its
575 " match and indent according to that.
576 if info.pline =~ '[[({]' || info.pline =~ '[])}]\s*\%(#.*\)\=$'
577 let [opening, closing] = s:ExtraBrackets(info.plnum)
578
579 if opening.pos != -1
Bram Moolenaar942db232021-02-13 18:14:48 +0100580 if !g:ruby_indent_hanging_elements
581 return indent(info.plnum) + info.sw
582 elseif opening.type == '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
Bram Moolenaard09091d2019-01-17 16:07:22 +0100583 if col('.') + 1 == col('$')
584 return indent(info.plnum) + info.sw
585 else
586 return virtcol('.')
587 endif
588 else
589 let nonspace = matchend(info.pline, '\S', opening.pos + 1) - 1
590 return nonspace > 0 ? nonspace : indent(info.plnum) + info.sw
591 endif
592 elseif closing.pos != -1
593 call cursor(info.plnum, closing.pos + 1)
594 normal! %
595
Bram Moolenaar46eea442022-03-30 10:51:39 +0100596 if strpart(info.pline, closing.pos) =~ '^)\s*='
597 " special case: the closing `) =` of an endless def
598 return indent(s:GetMSL(line('.')))
599 endif
600
Bram Moolenaard09091d2019-01-17 16:07:22 +0100601 if s:Match(line('.'), s:ruby_indent_keywords)
602 return indent('.') + info.sw
603 else
604 return indent(s:GetMSL(line('.')))
605 endif
606 else
607 call cursor(info.clnum, info.col)
608 end
609 endif
610
611 return -1
612endfunction
613
614function! s:AfterEndKeyword(pline_info) abort
615 let info = a:pline_info
616 " If the previous line ended with an "end", match that "end"s beginning's
617 " indent.
618 let col = s:Match(info.plnum, '\%(^\|[^.:@$]\)\<end\>\s*\%(#.*\)\=$')
619 if col > 0
620 call cursor(info.plnum, col)
621 if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW',
622 \ s:end_skip_expr) > 0
623 let n = line('.')
624 let ind = indent('.')
625 let msl = s:GetMSL(n)
626 if msl != n
627 let ind = indent(msl)
628 end
629 return ind
630 endif
631 end
632 return -1
633endfunction
634
635function! s:AfterIndentKeyword(pline_info) abort
636 let info = a:pline_info
637 let col = s:Match(info.plnum, s:ruby_indent_keywords)
638
Bram Moolenaar46eea442022-03-30 10:51:39 +0100639 if col > 0 && s:Match(info.plnum, s:ruby_endless_def) <= 0
Bram Moolenaard09091d2019-01-17 16:07:22 +0100640 call cursor(info.plnum, col)
641 let ind = virtcol('.') - 1 + info.sw
642 " TODO: make this better (we need to count them) (or, if a searchpair
643 " fails, we know that something is lacking an end and thus we indent a
644 " level
645 if s:Match(info.plnum, s:end_end_regex)
646 let ind = indent('.')
647 elseif s:IsAssignment(info.pline, col)
648 if g:ruby_indent_assignment_style == 'hanging'
649 " hanging indent
650 let ind = col + info.sw - 1
651 else
652 " align with variable
653 let ind = indent(info.plnum) + info.sw
654 endif
655 endif
656 return ind
657 endif
658
659 return -1
660endfunction
661
662function! s:PreviousNotMSL(msl_info) abort
663 let info = a:msl_info
664
665 " If the previous line wasn't a MSL
666 if info.plnum != info.plnum_msl
667 " If previous line ends bracket and begins non-bracket continuation decrease indent by 1.
668 if s:Match(info.plnum, s:bracket_switch_continuation_regex)
669 " TODO (2016-10-07) Wrong/unused? How could it be "1"?
670 return indent(info.plnum) - 1
671 " If previous line is a continuation return its indent.
Bram Moolenaar942db232021-02-13 18:14:48 +0100672 elseif s:Match(info.plnum, s:non_bracket_continuation_regex)
Bram Moolenaard09091d2019-01-17 16:07:22 +0100673 return indent(info.plnum)
674 endif
675 endif
676
677 return -1
678endfunction
679
680function! s:IndentingKeywordInMSL(msl_info) abort
681 let info = a:msl_info
682 " If the MSL line had an indenting keyword in it, add a level of indent.
683 " TODO: this does not take into account contrived things such as
684 " module Foo; class Bar; end
685 let col = s:Match(info.plnum_msl, s:ruby_indent_keywords)
Bram Moolenaar46eea442022-03-30 10:51:39 +0100686 if col > 0 && s:Match(info.plnum_msl, s:ruby_endless_def) <= 0
Bram Moolenaard09091d2019-01-17 16:07:22 +0100687 let ind = indent(info.plnum_msl) + info.sw
688 if s:Match(info.plnum_msl, s:end_end_regex)
689 let ind = ind - info.sw
690 elseif s:IsAssignment(getline(info.plnum_msl), col)
691 if g:ruby_indent_assignment_style == 'hanging'
692 " hanging indent
693 let ind = col + info.sw - 1
694 else
695 " align with variable
696 let ind = indent(info.plnum_msl) + info.sw
697 endif
698 endif
699 return ind
700 endif
701 return -1
702endfunction
703
704function! s:ContinuedHangingOperator(msl_info) abort
705 let info = a:msl_info
706
707 " If the previous line ended with [*+/.,-=], but wasn't a block ending or a
708 " closing bracket, indent one extra level.
709 if s:Match(info.plnum_msl, s:non_bracket_continuation_regex) && !s:Match(info.plnum_msl, '^\s*\([\])}]\|end\)')
710 if info.plnum_msl == info.plnum
711 let ind = indent(info.plnum_msl) + info.sw
712 else
713 let ind = indent(info.plnum_msl)
714 endif
715 return ind
716 endif
717
718 return -1
719endfunction
720
721" 4. Auxiliary Functions {{{1
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000722" ======================
723
Bram Moolenaard09091d2019-01-17 16:07:22 +0100724function! s:IsInRubyGroup(groups, lnum, col) abort
725 let ids = map(copy(a:groups), 'hlID("ruby".v:val)')
726 return index(ids, synID(a:lnum, a:col, 1)) >= 0
727endfunction
728
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000729" Check if the character at lnum:col is inside a string, comment, or is ascii.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100730function! s:IsInStringOrComment(lnum, col) abort
731 return s:IsInRubyGroup(s:syng_strcom, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000732endfunction
733
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000734" Check if the character at lnum:col is inside a string.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100735function! s:IsInString(lnum, col) abort
736 return s:IsInRubyGroup(s:syng_string, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000737endfunction
738
739" Check if the character at lnum:col is inside a string or documentation.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100740function! s:IsInStringOrDocumentation(lnum, col) abort
741 return s:IsInRubyGroup(s:syng_stringdoc, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000742endfunction
743
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200744" Check if the character at lnum:col is inside a string delimiter
Bram Moolenaard09091d2019-01-17 16:07:22 +0100745function! s:IsInStringDelimiter(lnum, col) abort
Bram Moolenaar2ed639a2019-12-09 23:11:18 +0100746 return s:IsInRubyGroup(
747 \ ['HeredocDelimiter', 'PercentStringDelimiter', 'StringDelimiter'],
748 \ a:lnum, a:col
749 \ )
Bram Moolenaard09091d2019-01-17 16:07:22 +0100750endfunction
751
752function! s:IsAssignment(str, pos) abort
753 return strpart(a:str, 0, a:pos - 1) =~ '=\s*$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200754endfunction
755
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000756" Find line above 'lnum' that isn't empty, in a comment, or in a string.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100757function! s:PrevNonBlankNonString(lnum) abort
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000758 let in_block = 0
759 let lnum = prevnonblank(a:lnum)
760 while lnum > 0
761 " Go in and out of blocks comments as necessary.
762 " If the line isn't empty (with opt. comment) or in a string, end search.
763 let line = getline(lnum)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200764 if line =~ '^=begin'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000765 if in_block
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200766 let in_block = 0
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000767 else
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200768 break
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000769 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200770 elseif !in_block && line =~ '^=end'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000771 let in_block = 1
772 elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200773 \ && s:IsInStringOrComment(lnum, strlen(line)))
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000774 break
775 endif
776 let lnum = prevnonblank(lnum - 1)
777 endwhile
778 return lnum
779endfunction
780
781" Find line above 'lnum' that started the continuation 'lnum' may be part of.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100782function! s:GetMSL(lnum) abort
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000783 " Start on the line we're at and use its indent.
784 let msl = a:lnum
785 let lnum = s:PrevNonBlankNonString(a:lnum - 1)
786 while lnum > 0
787 " If we have a continuation line, or we're in a string, use line as MSL.
788 " Otherwise, terminate search as we have found our MSL already.
789 let line = getline(lnum)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200790
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200791 if !s:Match(msl, s:backslash_continuation_regex) &&
792 \ s:Match(lnum, s:backslash_continuation_regex)
793 " If the current line doesn't end in a backslash, but the previous one
794 " does, look for that line's msl
795 "
796 " Example:
797 " foo = "bar" \
798 " "baz"
799 "
800 let msl = lnum
801 elseif s:Match(msl, s:leading_operator_regex)
802 " If the current line starts with a leading operator, keep its indent
803 " and keep looking for an MSL.
804 let msl = lnum
805 elseif s:Match(lnum, s:splat_regex)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200806 " If the above line looks like the "*" of a splat, use the current one's
807 " indentation.
808 "
809 " Example:
810 " Hash[*
811 " method_call do
812 " something
813 "
814 return msl
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200815 elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200816 \ s:Match(msl, s:non_bracket_continuation_regex)
817 " If the current line is a non-bracket continuation and so is the
818 " previous one, keep its indent and continue looking for an MSL.
819 "
820 " Example:
821 " method_call one,
822 " two,
823 " three
824 "
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000825 let msl = lnum
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200826 elseif s:Match(lnum, s:dot_continuation_regex) &&
827 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
828 " If the current line is a bracket continuation or a block-starter, but
829 " the previous is a dot, keep going to see if the previous line is the
830 " start of another continuation.
831 "
832 " Example:
833 " parent.
834 " method_call {
835 " three
836 "
837 let msl = lnum
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200838 elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
839 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
840 " If the current line is a bracket continuation or a block-starter, but
841 " the previous is a non-bracket one, respect the previous' indentation,
842 " and stop here.
843 "
844 " Example:
845 " method_call one,
846 " two {
847 " three
848 "
849 return lnum
850 elseif s:Match(lnum, s:bracket_continuation_regex) &&
851 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
852 " If both lines are bracket continuations (the current may also be a
853 " block-starter), use the current one's and stop here
854 "
855 " Example:
856 " method_call(
857 " other_method_call(
858 " foo
859 return msl
860 elseif s:Match(lnum, s:block_regex) &&
861 \ !s:Match(msl, s:continuation_regex) &&
862 \ !s:Match(msl, s:block_continuation_regex)
863 " If the previous line is a block-starter and the current one is
864 " mostly ordinary, use the current one as the MSL.
865 "
866 " Example:
867 " method_call do
868 " something
869 " something_else
870 return msl
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000871 else
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200872 let col = match(line, s:continuation_regex) + 1
873 if (col > 0 && !s:IsInStringOrComment(lnum, col))
874 \ || s:IsInString(lnum, strlen(line))
875 let msl = lnum
876 else
877 break
878 endif
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000879 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200880
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000881 let lnum = s:PrevNonBlankNonString(lnum - 1)
882 endwhile
883 return msl
884endfunction
885
886" Check if line 'lnum' has more opening brackets than closing ones.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100887function! s:ExtraBrackets(lnum) abort
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200888 let opening = {'parentheses': [], 'braces': [], 'brackets': []}
889 let closing = {'parentheses': [], 'braces': [], 'brackets': []}
890
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000891 let line = getline(a:lnum)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200892 let pos = match(line, '[][(){}]', 0)
893
894 " Save any encountered opening brackets, and remove them once a matching
895 " closing one has been found. If a closing bracket shows up that doesn't
896 " close anything, save it for later.
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000897 while pos != -1
898 if !s:IsInStringOrComment(a:lnum, pos + 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200899 if line[pos] == '('
900 call add(opening.parentheses, {'type': '(', 'pos': pos})
901 elseif line[pos] == ')'
902 if empty(opening.parentheses)
903 call add(closing.parentheses, {'type': ')', 'pos': pos})
904 else
905 let opening.parentheses = opening.parentheses[0:-2]
906 endif
907 elseif line[pos] == '{'
908 call add(opening.braces, {'type': '{', 'pos': pos})
909 elseif line[pos] == '}'
910 if empty(opening.braces)
911 call add(closing.braces, {'type': '}', 'pos': pos})
912 else
913 let opening.braces = opening.braces[0:-2]
914 endif
915 elseif line[pos] == '['
916 call add(opening.brackets, {'type': '[', 'pos': pos})
917 elseif line[pos] == ']'
918 if empty(opening.brackets)
919 call add(closing.brackets, {'type': ']', 'pos': pos})
920 else
921 let opening.brackets = opening.brackets[0:-2]
922 endif
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000923 endif
924 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200925
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000926 let pos = match(line, '[][(){}]', pos + 1)
927 endwhile
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200928
929 " Find the rightmost brackets, since they're the ones that are important in
930 " both opening and closing cases
931 let rightmost_opening = {'type': '(', 'pos': -1}
932 let rightmost_closing = {'type': ')', 'pos': -1}
933
934 for opening in opening.parentheses + opening.braces + opening.brackets
935 if opening.pos > rightmost_opening.pos
936 let rightmost_opening = opening
937 endif
938 endfor
939
940 for closing in closing.parentheses + closing.braces + closing.brackets
941 if closing.pos > rightmost_closing.pos
942 let rightmost_closing = closing
943 endif
944 endfor
945
946 return [rightmost_opening, rightmost_closing]
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000947endfunction
948
Bram Moolenaard09091d2019-01-17 16:07:22 +0100949function! s:Match(lnum, regex) abort
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200950 let line = getline(a:lnum)
951 let offset = match(line, '\C'.a:regex)
952 let col = offset + 1
953
954 while offset > -1 && s:IsInStringOrComment(a:lnum, col)
955 let offset = match(line, '\C'.a:regex, offset + 1)
956 let col = offset + 1
957 endwhile
958
959 if offset > -1
960 return col
961 else
962 return 0
963 endif
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000964endfunction
965
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200966" Locates the containing class/module's definition line, ignoring nested classes
967" along the way.
968"
Bram Moolenaard09091d2019-01-17 16:07:22 +0100969function! s:FindContainingClass() abort
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200970 let saved_position = getpos('.')
971
972 while searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
973 \ s:end_skip_expr) > 0
974 if expand('<cword>') =~# '\<class\|module\>'
975 let found_lnum = line('.')
976 call setpos('.', saved_position)
977 return found_lnum
978 endif
Bram Moolenaar45758762016-10-12 23:08:06 +0200979 endwhile
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200980
981 call setpos('.', saved_position)
982 return 0
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000983endfunction
984
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000985" }}}1
986
987let &cpo = s:cpo_save
988unlet s:cpo_save
Bram Moolenaar9964e462007-05-05 17:54:07 +0000989
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200990" vim:set sw=2 sts=2 ts=8 et: