blob: 23b61fdfe08f0eef90915d3a4d3ba611bc03fe7b [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 Moolenaar2ed639a2019-12-09 23:11:18 +01007" Last Change: 2019 Dec 08
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"
30 let g:ruby_indent_block_style = 'expression'
31endif
32
Bram Moolenaar551dbcc2006-04-25 22:13:59 +000033setlocal nosmartindent
34
Bram Moolenaar60a795a2005-09-16 21:55:43 +000035" Now, set up our indentation expression and keys that trigger it.
Bram Moolenaarec7944a2013-06-12 21:29:15 +020036setlocal indentexpr=GetRubyIndent(v:lnum)
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020037setlocal indentkeys=0{,0},0),0],!^F,o,O,e,:,.
Bram Moolenaarec7944a2013-06-12 21:29:15 +020038setlocal indentkeys+==end,=else,=elsif,=when,=ensure,=rescue,==begin,==end
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020039setlocal indentkeys+==private,=protected,=public
Bram Moolenaar071d4272004-06-13 20:20:40 +000040
41" Only define the function once.
42if exists("*GetRubyIndent")
43 finish
44endif
45
Bram Moolenaar60a795a2005-09-16 21:55:43 +000046let s:cpo_save = &cpo
47set cpo&vim
48
49" 1. Variables {{{1
50" ============
51
Bram Moolenaard09091d2019-01-17 16:07:22 +010052" Syntax group names that are strings.
Bram Moolenaar60a795a2005-09-16 21:55:43 +000053let s:syng_string =
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010054 \ ['String', 'Interpolation', 'InterpolationDelimiter', 'StringEscape']
Bram Moolenaar60a795a2005-09-16 21:55:43 +000055
Bram Moolenaard09091d2019-01-17 16:07:22 +010056" Syntax group names that are strings or documentation.
57let s:syng_stringdoc = s:syng_string + ['Documentation']
58
59" Syntax group names that are or delimit strings/symbols/regexes or are comments.
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010060let s:syng_strcom = s:syng_stringdoc + [
61 \ 'Character',
62 \ 'Comment',
63 \ 'HeredocDelimiter',
64 \ 'PercentRegexpDelimiter',
65 \ 'PercentStringDelimiter',
66 \ 'PercentSymbolDelimiter',
67 \ 'Regexp',
68 \ 'RegexpCharClass',
69 \ 'RegexpDelimiter',
70 \ 'RegexpEscape',
71 \ 'StringDelimiter',
72 \ 'Symbol',
73 \ 'SymbolDelimiter',
74 \ ]
Bram Moolenaar60a795a2005-09-16 21:55:43 +000075
76" Expression used to check whether we should skip a match with searchpair().
77let s:skip_expr =
Bram Moolenaard09091d2019-01-17 16:07:22 +010078 \ 'index(map('.string(s:syng_strcom).',"hlID(''ruby''.v:val)"), synID(line("."),col("."),1)) >= 0'
Bram Moolenaar60a795a2005-09-16 21:55:43 +000079
80" Regex used for words that, at the start of a line, add a level of indent.
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020081let s:ruby_indent_keywords =
82 \ '^\s*\zs\<\%(module\|class\|if\|for' .
83 \ '\|while\|until\|else\|elsif\|case\|when\|unless\|begin\|ensure\|rescue' .
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010084 \ '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' .
Bram Moolenaarec7944a2013-06-12 21:29:15 +020085 \ '\|\%([=,*/%+-]\|<<\|>>\|:\s\)\s*\zs' .
86 \ '\<\%(if\|for\|while\|until\|case\|unless\|begin\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +000087
88" Regex used for words that, at the start of a line, remove a level of indent.
89let s:ruby_deindent_keywords =
Bram Moolenaarec7944a2013-06-12 21:29:15 +020090 \ '^\s*\zs\<\%(ensure\|else\|rescue\|elsif\|when\|end\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +000091
92" Regex that defines the start-match for the 'end' keyword.
93"let s:end_start_regex = '\%(^\|[^.]\)\<\%(module\|class\|def\|if\|for\|while\|until\|case\|unless\|begin\|do\)\>'
94" TODO: the do here should be restricted somewhat (only at end of line)?
Bram Moolenaarec7944a2013-06-12 21:29:15 +020095let s:end_start_regex =
96 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
Bram Moolenaar89bcfda2016-08-30 23:26:57 +020097 \ '\<\%(module\|class\|if\|for\|while\|until\|case\|unless\|begin' .
Bram Moolenaar2ed639a2019-12-09 23:11:18 +010098 \ '\|\%(\K\k*[!?]\?\s\+\)\=def\):\@!\>' .
Bram Moolenaarec7944a2013-06-12 21:29:15 +020099 \ '\|\%(^\|[^.:@$]\)\@<=\<do:\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000100
101" Regex that defines the middle-match for the 'end' keyword.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200102let s:end_middle_regex = '\<\%(ensure\|else\|\%(\%(^\|;\)\s*\)\@<=\<rescue:\@!\>\|when\|elsif\):\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000103
104" Regex that defines the end-match for the 'end' keyword.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200105let s:end_end_regex = '\%(^\|[^.:@$]\)\@<=\<end:\@!\>'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000106
107" Expression used for searchpair() call for finding match for 'end' keyword.
108let s:end_skip_expr = s:skip_expr .
109 \ ' || (expand("<cword>") == "do"' .
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200110 \ ' && getline(".") =~ "^\\s*\\<\\(while\\|until\\|for\\):\\@!\\>")'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000111
112" Regex that defines continuation lines, not including (, {, or [.
Bram Moolenaar45758762016-10-12 23:08:06 +0200113let s:non_bracket_continuation_regex =
114 \ '\%([\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000115
116" Regex that defines continuation lines.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200117let s:continuation_regex =
Bram Moolenaar45758762016-10-12 23:08:06 +0200118 \ '\%(%\@<![({[\\.,:*/%+]\|\<and\|\<or\|\%(<%\)\@<![=-]\|:\@<![^[:alnum:]:][|&?]\|||\|&&\)\s*\%(#.*\)\=$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200119
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200120" Regex that defines continuable keywords
121let s:continuable_regex =
122 \ '\C\%(^\s*\|[=,*/%+\-|;{]\|<<\|>>\|:\s\)\s*\zs' .
123 \ '\<\%(if\|for\|while\|until\|unless\):\@!\>'
124
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200125" Regex that defines bracket continuations
126let s:bracket_continuation_regex = '%\@<!\%([({[]\)\s*\%(#.*\)\=$'
127
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200128" Regex that defines dot continuations
129let s:dot_continuation_regex = '%\@<!\.\s*\%(#.*\)\=$'
130
131" Regex that defines backslash continuations
132let s:backslash_continuation_regex = '%\@<!\\\s*$'
133
134" Regex that defines end of bracket continuation followed by another continuation
135let s:bracket_switch_continuation_regex = '^\([^(]\+\zs).\+\)\+'.s:continuation_regex
136
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200137" Regex that defines the first part of a splat pattern
138let s:splat_regex = '[[,(]\s*\*\s*\%(#.*\)\=$'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000139
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200140" Regex that describes all indent access modifiers
141let s:access_modifier_regex = '\C^\s*\%(public\|protected\|private\)\s*\%(#.*\)\=$'
142
143" Regex that describes the indent access modifiers (excludes public)
144let s:indent_access_modifier_regex = '\C^\s*\%(protected\|private\)\s*\%(#.*\)\=$'
145
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000146" Regex that defines blocks.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200147"
148" Note that there's a slight problem with this regex and s:continuation_regex.
149" Code like this will be matched by both:
150"
151" method_call do |(a, b)|
152"
153" The reason is that the pipe matches a hanging "|" operator.
154"
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000155let s:block_regex =
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200156 \ '\%(\<do:\@!\>\|%\@<!{\)\s*\%(|[^|]*|\)\=\s*\%(#.*\)\=$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200157
158let s:block_continuation_regex = '^\s*[^])}\t ].*'.s:block_regex
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000159
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200160" Regex that describes a leading operator (only a method call's dot for now)
Bram Moolenaar2ed639a2019-12-09 23:11:18 +0100161let s:leading_operator_regex = '^\s*\%(&\=\.\)'
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200162
Bram Moolenaard09091d2019-01-17 16:07:22 +0100163" 2. GetRubyIndent Function {{{1
164" =========================
165
166function! GetRubyIndent(...) abort
167 " 2.1. Setup {{{2
168 " ----------
169
170 let indent_info = {}
171
172 " The value of a single shift-width
173 if exists('*shiftwidth')
174 let indent_info.sw = shiftwidth()
175 else
176 let indent_info.sw = &sw
177 endif
178
179 " For the current line, use the first argument if given, else v:lnum
180 let indent_info.clnum = a:0 ? a:1 : v:lnum
181 let indent_info.cline = getline(indent_info.clnum)
182
183 " Set up variables for restoring position in file. Could use clnum here.
184 let indent_info.col = col('.')
185
186 " 2.2. Work on the current line {{{2
187 " -----------------------------
188 let indent_callback_names = [
189 \ 's:AccessModifier',
190 \ 's:ClosingBracketOnEmptyLine',
191 \ 's:BlockComment',
192 \ 's:DeindentingKeyword',
193 \ 's:MultilineStringOrLineComment',
194 \ 's:ClosingHeredocDelimiter',
195 \ 's:LeadingOperator',
196 \ ]
197
198 for callback_name in indent_callback_names
199" Decho "Running: ".callback_name
200 let indent = call(function(callback_name), [indent_info])
201
202 if indent >= 0
203" Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
204 return indent
205 endif
206 endfor
207
208 " 2.3. Work on the previous line. {{{2
209 " -------------------------------
210
211 " Special case: we don't need the real s:PrevNonBlankNonString for an empty
212 " line inside a string. And that call can be quite expensive in that
213 " particular situation.
214 let indent_callback_names = [
215 \ 's:EmptyInsideString',
216 \ ]
217
218 for callback_name in indent_callback_names
219" Decho "Running: ".callback_name
220 let indent = call(function(callback_name), [indent_info])
221
222 if indent >= 0
223" Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
224 return indent
225 endif
226 endfor
227
228 " Previous line number
229 let indent_info.plnum = s:PrevNonBlankNonString(indent_info.clnum - 1)
230 let indent_info.pline = getline(indent_info.plnum)
231
232 let indent_callback_names = [
233 \ 's:StartOfFile',
234 \ 's:AfterAccessModifier',
235 \ 's:ContinuedLine',
236 \ 's:AfterBlockOpening',
237 \ 's:AfterHangingSplat',
238 \ 's:AfterUnbalancedBracket',
239 \ 's:AfterLeadingOperator',
240 \ 's:AfterEndKeyword',
241 \ 's:AfterIndentKeyword',
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 " 2.4. Work on the MSL line. {{{2
255 " --------------------------
256 let indent_callback_names = [
257 \ 's:PreviousNotMSL',
258 \ 's:IndentingKeywordInMSL',
259 \ 's:ContinuedHangingOperator',
260 \ ]
261
262 " Most Significant line based on the previous one -- in case it's a
263 " contination of something above
264 let indent_info.plnum_msl = s:GetMSL(indent_info.plnum)
265
266 for callback_name in indent_callback_names
267" Decho "Running: ".callback_name
268 let indent = call(function(callback_name), [indent_info])
269
270 if indent >= 0
271" Decho "Match: ".callback_name." indent=".indent." info=".string(indent_info)
272 return indent
273 endif
274 endfor
275
276 " }}}2
277
278 " By default, just return the previous line's indent
279" Decho "Default case matched"
280 return indent(indent_info.plnum)
281endfunction
282
283" 3. Indenting Logic Callbacks {{{1
284" ============================
285
286function! s:AccessModifier(cline_info) abort
287 let info = a:cline_info
288
289 " If this line is an access modifier keyword, align according to the closest
290 " class declaration.
291 if g:ruby_indent_access_modifier_style == 'indent'
292 if s:Match(info.clnum, s:access_modifier_regex)
293 let class_lnum = s:FindContainingClass()
294 if class_lnum > 0
295 return indent(class_lnum) + info.sw
296 endif
297 endif
298 elseif g:ruby_indent_access_modifier_style == 'outdent'
299 if s:Match(info.clnum, s:access_modifier_regex)
300 let class_lnum = s:FindContainingClass()
301 if class_lnum > 0
302 return indent(class_lnum)
303 endif
304 endif
305 endif
306
307 return -1
308endfunction
309
310function! s:ClosingBracketOnEmptyLine(cline_info) abort
311 let info = a:cline_info
312
313 " If we got a closing bracket on an empty line, find its match and indent
314 " according to it. For parentheses we indent to its column - 1, for the
315 " others we indent to the containing line's MSL's level. Return -1 if fail.
316 let col = matchend(info.cline, '^\s*[]})]')
317
318 if col > 0 && !s:IsInStringOrComment(info.clnum, col)
319 call cursor(info.clnum, col)
320 let closing_bracket = info.cline[col - 1]
321 let bracket_pair = strpart('(){}[]', stridx(')}]', closing_bracket) * 2, 2)
322
323 if searchpair(escape(bracket_pair[0], '\['), '', bracket_pair[1], 'bW', s:skip_expr) > 0
324 if closing_bracket == ')' && col('.') != col('$') - 1
325 let ind = virtcol('.') - 1
326 elseif g:ruby_indent_block_style == 'do'
327 let ind = indent(line('.'))
328 else " g:ruby_indent_block_style == 'expression'
329 let ind = indent(s:GetMSL(line('.')))
330 endif
331 endif
332
333 return ind
334 endif
335
336 return -1
337endfunction
338
339function! s:BlockComment(cline_info) abort
340 " If we have a =begin or =end set indent to first column.
341 if match(a:cline_info.cline, '^\s*\%(=begin\|=end\)$') != -1
342 return 0
343 endif
344 return -1
345endfunction
346
347function! s:DeindentingKeyword(cline_info) abort
348 let info = a:cline_info
349
350 " If we have a deindenting keyword, find its match and indent to its level.
351 " TODO: this is messy
352 if s:Match(info.clnum, s:ruby_deindent_keywords)
353 call cursor(info.clnum, 1)
354
355 if searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
356 \ s:end_skip_expr) > 0
357 let msl = s:GetMSL(line('.'))
358 let line = getline(line('.'))
359
360 if s:IsAssignment(line, col('.')) &&
361 \ strpart(line, col('.') - 1, 2) !~ 'do'
362 " assignment to case/begin/etc, on the same line
363 if g:ruby_indent_assignment_style == 'hanging'
364 " hanging indent
365 let ind = virtcol('.') - 1
366 else
367 " align with variable
368 let ind = indent(line('.'))
369 endif
370 elseif g:ruby_indent_block_style == 'do'
371 " align to line of the "do", not to the MSL
372 let ind = indent(line('.'))
373 elseif getline(msl) =~ '=\s*\(#.*\)\=$'
374 " in the case of assignment to the MSL, align to the starting line,
375 " not to the MSL
376 let ind = indent(line('.'))
377 else
378 " align to the MSL
379 let ind = indent(msl)
380 endif
381 endif
382 return ind
383 endif
384
385 return -1
386endfunction
387
388function! s:MultilineStringOrLineComment(cline_info) abort
389 let info = a:cline_info
390
391 " If we are in a multi-line string or line-comment, don't do anything to it.
392 if s:IsInStringOrDocumentation(info.clnum, matchend(info.cline, '^\s*') + 1)
393 return indent(info.clnum)
394 endif
395 return -1
396endfunction
397
398function! s:ClosingHeredocDelimiter(cline_info) abort
399 let info = a:cline_info
400
401 " If we are at the closing delimiter of a "<<" heredoc-style string, set the
402 " indent to 0.
403 if info.cline =~ '^\k\+\s*$'
404 \ && s:IsInStringDelimiter(info.clnum, 1)
405 \ && search('\V<<'.info.cline, 'nbW') > 0
406 return 0
407 endif
408
409 return -1
410endfunction
411
412function! s:LeadingOperator(cline_info) abort
413 " If the current line starts with a leading operator, add a level of indent.
414 if s:Match(a:cline_info.clnum, s:leading_operator_regex)
415 return indent(s:GetMSL(a:cline_info.clnum)) + a:cline_info.sw
416 endif
417 return -1
418endfunction
419
420function! s:EmptyInsideString(pline_info) abort
421 " If the line is empty and inside a string (the previous line is a string,
422 " too), use the previous line's indent
423 let info = a:pline_info
424
425 let plnum = prevnonblank(info.clnum - 1)
426 let pline = getline(plnum)
427
428 if info.cline =~ '^\s*$'
429 \ && s:IsInStringOrComment(plnum, 1)
430 \ && s:IsInStringOrComment(plnum, strlen(pline))
431 return indent(plnum)
432 endif
433 return -1
434endfunction
435
436function! s:StartOfFile(pline_info) abort
437 " At the start of the file use zero indent.
438 if a:pline_info.plnum == 0
439 return 0
440 endif
441 return -1
442endfunction
443
444function! s:AfterAccessModifier(pline_info) abort
445 let info = a:pline_info
446
447 if g:ruby_indent_access_modifier_style == 'indent'
448 " If the previous line was a private/protected keyword, add a
449 " level of indent.
450 if s:Match(info.plnum, s:indent_access_modifier_regex)
451 return indent(info.plnum) + info.sw
452 endif
453 elseif g:ruby_indent_access_modifier_style == 'outdent'
454 " If the previous line was a private/protected/public keyword, add
455 " a level of indent, since the keyword has been out-dented.
456 if s:Match(info.plnum, s:access_modifier_regex)
457 return indent(info.plnum) + info.sw
458 endif
459 endif
460 return -1
461endfunction
462
463" Example:
464"
465" if foo || bar ||
466" baz || bing
467" puts "foo"
468" end
469"
470function! s:ContinuedLine(pline_info) abort
471 let info = a:pline_info
472
473 let col = s:Match(info.plnum, s:ruby_indent_keywords)
474 if s:Match(info.plnum, s:continuable_regex) &&
475 \ s:Match(info.plnum, s:continuation_regex)
476 if col > 0 && s:IsAssignment(info.pline, col)
477 if g:ruby_indent_assignment_style == 'hanging'
478 " hanging indent
479 let ind = col - 1
480 else
481 " align with variable
482 let ind = indent(info.plnum)
483 endif
484 else
485 let ind = indent(s:GetMSL(info.plnum))
486 endif
487 return ind + info.sw + info.sw
488 endif
489 return -1
490endfunction
491
492function! s:AfterBlockOpening(pline_info) abort
493 let info = a:pline_info
494
495 " If the previous line ended with a block opening, add a level of indent.
496 if s:Match(info.plnum, s:block_regex)
497 if g:ruby_indent_block_style == 'do'
498 " don't align to the msl, align to the "do"
499 let ind = indent(info.plnum) + info.sw
500 else
501 let plnum_msl = s:GetMSL(info.plnum)
502
503 if getline(plnum_msl) =~ '=\s*\(#.*\)\=$'
504 " in the case of assignment to the msl, align to the starting line,
505 " not to the msl
506 let ind = indent(info.plnum) + info.sw
507 else
508 let ind = indent(plnum_msl) + info.sw
509 endif
510 endif
511
512 return ind
513 endif
514
515 return -1
516endfunction
517
518function! s:AfterLeadingOperator(pline_info) abort
519 " If the previous line started with a leading operator, use its MSL's level
520 " of indent
521 if s:Match(a:pline_info.plnum, s:leading_operator_regex)
522 return indent(s:GetMSL(a:pline_info.plnum))
523 endif
524 return -1
525endfunction
526
527function! s:AfterHangingSplat(pline_info) abort
528 let info = a:pline_info
529
530 " If the previous line ended with the "*" of a splat, add a level of indent
531 if info.pline =~ s:splat_regex
532 return indent(info.plnum) + info.sw
533 endif
534 return -1
535endfunction
536
537function! s:AfterUnbalancedBracket(pline_info) abort
538 let info = a:pline_info
539
540 " If the previous line contained unclosed opening brackets and we are still
541 " in them, find the rightmost one and add indent depending on the bracket
542 " type.
543 "
544 " If it contained hanging closing brackets, find the rightmost one, find its
545 " match and indent according to that.
546 if info.pline =~ '[[({]' || info.pline =~ '[])}]\s*\%(#.*\)\=$'
547 let [opening, closing] = s:ExtraBrackets(info.plnum)
548
549 if opening.pos != -1
550 if opening.type == '(' && searchpair('(', '', ')', 'bW', s:skip_expr) > 0
551 if col('.') + 1 == col('$')
552 return indent(info.plnum) + info.sw
553 else
554 return virtcol('.')
555 endif
556 else
557 let nonspace = matchend(info.pline, '\S', opening.pos + 1) - 1
558 return nonspace > 0 ? nonspace : indent(info.plnum) + info.sw
559 endif
560 elseif closing.pos != -1
561 call cursor(info.plnum, closing.pos + 1)
562 normal! %
563
564 if s:Match(line('.'), s:ruby_indent_keywords)
565 return indent('.') + info.sw
566 else
567 return indent(s:GetMSL(line('.')))
568 endif
569 else
570 call cursor(info.clnum, info.col)
571 end
572 endif
573
574 return -1
575endfunction
576
577function! s:AfterEndKeyword(pline_info) abort
578 let info = a:pline_info
579 " If the previous line ended with an "end", match that "end"s beginning's
580 " indent.
581 let col = s:Match(info.plnum, '\%(^\|[^.:@$]\)\<end\>\s*\%(#.*\)\=$')
582 if col > 0
583 call cursor(info.plnum, col)
584 if searchpair(s:end_start_regex, '', s:end_end_regex, 'bW',
585 \ s:end_skip_expr) > 0
586 let n = line('.')
587 let ind = indent('.')
588 let msl = s:GetMSL(n)
589 if msl != n
590 let ind = indent(msl)
591 end
592 return ind
593 endif
594 end
595 return -1
596endfunction
597
598function! s:AfterIndentKeyword(pline_info) abort
599 let info = a:pline_info
600 let col = s:Match(info.plnum, s:ruby_indent_keywords)
601
602 if col > 0
603 call cursor(info.plnum, col)
604 let ind = virtcol('.') - 1 + info.sw
605 " TODO: make this better (we need to count them) (or, if a searchpair
606 " fails, we know that something is lacking an end and thus we indent a
607 " level
608 if s:Match(info.plnum, s:end_end_regex)
609 let ind = indent('.')
610 elseif s:IsAssignment(info.pline, col)
611 if g:ruby_indent_assignment_style == 'hanging'
612 " hanging indent
613 let ind = col + info.sw - 1
614 else
615 " align with variable
616 let ind = indent(info.plnum) + info.sw
617 endif
618 endif
619 return ind
620 endif
621
622 return -1
623endfunction
624
625function! s:PreviousNotMSL(msl_info) abort
626 let info = a:msl_info
627
628 " If the previous line wasn't a MSL
629 if info.plnum != info.plnum_msl
630 " If previous line ends bracket and begins non-bracket continuation decrease indent by 1.
631 if s:Match(info.plnum, s:bracket_switch_continuation_regex)
632 " TODO (2016-10-07) Wrong/unused? How could it be "1"?
633 return indent(info.plnum) - 1
634 " If previous line is a continuation return its indent.
635 " TODO: the || s:IsInString() thing worries me a bit.
636 elseif s:Match(info.plnum, s:non_bracket_continuation_regex) || s:IsInString(info.plnum, strlen(line))
637 return indent(info.plnum)
638 endif
639 endif
640
641 return -1
642endfunction
643
644function! s:IndentingKeywordInMSL(msl_info) abort
645 let info = a:msl_info
646 " If the MSL line had an indenting keyword in it, add a level of indent.
647 " TODO: this does not take into account contrived things such as
648 " module Foo; class Bar; end
649 let col = s:Match(info.plnum_msl, s:ruby_indent_keywords)
650 if col > 0
651 let ind = indent(info.plnum_msl) + info.sw
652 if s:Match(info.plnum_msl, s:end_end_regex)
653 let ind = ind - info.sw
654 elseif s:IsAssignment(getline(info.plnum_msl), col)
655 if g:ruby_indent_assignment_style == 'hanging'
656 " hanging indent
657 let ind = col + info.sw - 1
658 else
659 " align with variable
660 let ind = indent(info.plnum_msl) + info.sw
661 endif
662 endif
663 return ind
664 endif
665 return -1
666endfunction
667
668function! s:ContinuedHangingOperator(msl_info) abort
669 let info = a:msl_info
670
671 " If the previous line ended with [*+/.,-=], but wasn't a block ending or a
672 " closing bracket, indent one extra level.
673 if s:Match(info.plnum_msl, s:non_bracket_continuation_regex) && !s:Match(info.plnum_msl, '^\s*\([\])}]\|end\)')
674 if info.plnum_msl == info.plnum
675 let ind = indent(info.plnum_msl) + info.sw
676 else
677 let ind = indent(info.plnum_msl)
678 endif
679 return ind
680 endif
681
682 return -1
683endfunction
684
685" 4. Auxiliary Functions {{{1
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000686" ======================
687
Bram Moolenaard09091d2019-01-17 16:07:22 +0100688function! s:IsInRubyGroup(groups, lnum, col) abort
689 let ids = map(copy(a:groups), 'hlID("ruby".v:val)')
690 return index(ids, synID(a:lnum, a:col, 1)) >= 0
691endfunction
692
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000693" Check if the character at lnum:col is inside a string, comment, or is ascii.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100694function! s:IsInStringOrComment(lnum, col) abort
695 return s:IsInRubyGroup(s:syng_strcom, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000696endfunction
697
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000698" Check if the character at lnum:col is inside a string.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100699function! s:IsInString(lnum, col) abort
700 return s:IsInRubyGroup(s:syng_string, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000701endfunction
702
703" Check if the character at lnum:col is inside a string or documentation.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100704function! s:IsInStringOrDocumentation(lnum, col) abort
705 return s:IsInRubyGroup(s:syng_stringdoc, a:lnum, a:col)
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000706endfunction
707
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200708" Check if the character at lnum:col is inside a string delimiter
Bram Moolenaard09091d2019-01-17 16:07:22 +0100709function! s:IsInStringDelimiter(lnum, col) abort
Bram Moolenaar2ed639a2019-12-09 23:11:18 +0100710 return s:IsInRubyGroup(
711 \ ['HeredocDelimiter', 'PercentStringDelimiter', 'StringDelimiter'],
712 \ a:lnum, a:col
713 \ )
Bram Moolenaard09091d2019-01-17 16:07:22 +0100714endfunction
715
716function! s:IsAssignment(str, pos) abort
717 return strpart(a:str, 0, a:pos - 1) =~ '=\s*$'
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200718endfunction
719
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000720" Find line above 'lnum' that isn't empty, in a comment, or in a string.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100721function! s:PrevNonBlankNonString(lnum) abort
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000722 let in_block = 0
723 let lnum = prevnonblank(a:lnum)
724 while lnum > 0
725 " Go in and out of blocks comments as necessary.
726 " If the line isn't empty (with opt. comment) or in a string, end search.
727 let line = getline(lnum)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200728 if line =~ '^=begin'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000729 if in_block
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200730 let in_block = 0
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000731 else
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200732 break
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000733 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200734 elseif !in_block && line =~ '^=end'
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000735 let in_block = 1
736 elseif !in_block && line !~ '^\s*#.*$' && !(s:IsInStringOrComment(lnum, 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200737 \ && s:IsInStringOrComment(lnum, strlen(line)))
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000738 break
739 endif
740 let lnum = prevnonblank(lnum - 1)
741 endwhile
742 return lnum
743endfunction
744
745" Find line above 'lnum' that started the continuation 'lnum' may be part of.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100746function! s:GetMSL(lnum) abort
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000747 " Start on the line we're at and use its indent.
748 let msl = a:lnum
749 let lnum = s:PrevNonBlankNonString(a:lnum - 1)
750 while lnum > 0
751 " If we have a continuation line, or we're in a string, use line as MSL.
752 " Otherwise, terminate search as we have found our MSL already.
753 let line = getline(lnum)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200754
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200755 if !s:Match(msl, s:backslash_continuation_regex) &&
756 \ s:Match(lnum, s:backslash_continuation_regex)
757 " If the current line doesn't end in a backslash, but the previous one
758 " does, look for that line's msl
759 "
760 " Example:
761 " foo = "bar" \
762 " "baz"
763 "
764 let msl = lnum
765 elseif s:Match(msl, s:leading_operator_regex)
766 " If the current line starts with a leading operator, keep its indent
767 " and keep looking for an MSL.
768 let msl = lnum
769 elseif s:Match(lnum, s:splat_regex)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200770 " If the above line looks like the "*" of a splat, use the current one's
771 " indentation.
772 "
773 " Example:
774 " Hash[*
775 " method_call do
776 " something
777 "
778 return msl
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200779 elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200780 \ s:Match(msl, s:non_bracket_continuation_regex)
781 " If the current line is a non-bracket continuation and so is the
782 " previous one, keep its indent and continue looking for an MSL.
783 "
784 " Example:
785 " method_call one,
786 " two,
787 " three
788 "
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000789 let msl = lnum
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200790 elseif s:Match(lnum, s:dot_continuation_regex) &&
791 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
792 " If the current line is a bracket continuation or a block-starter, but
793 " the previous is a dot, keep going to see if the previous line is the
794 " start of another continuation.
795 "
796 " Example:
797 " parent.
798 " method_call {
799 " three
800 "
801 let msl = lnum
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200802 elseif s:Match(lnum, s:non_bracket_continuation_regex) &&
803 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
804 " If the current line is a bracket continuation or a block-starter, but
805 " the previous is a non-bracket one, respect the previous' indentation,
806 " and stop here.
807 "
808 " Example:
809 " method_call one,
810 " two {
811 " three
812 "
813 return lnum
814 elseif s:Match(lnum, s:bracket_continuation_regex) &&
815 \ (s:Match(msl, s:bracket_continuation_regex) || s:Match(msl, s:block_continuation_regex))
816 " If both lines are bracket continuations (the current may also be a
817 " block-starter), use the current one's and stop here
818 "
819 " Example:
820 " method_call(
821 " other_method_call(
822 " foo
823 return msl
824 elseif s:Match(lnum, s:block_regex) &&
825 \ !s:Match(msl, s:continuation_regex) &&
826 \ !s:Match(msl, s:block_continuation_regex)
827 " If the previous line is a block-starter and the current one is
828 " mostly ordinary, use the current one as the MSL.
829 "
830 " Example:
831 " method_call do
832 " something
833 " something_else
834 return msl
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000835 else
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200836 let col = match(line, s:continuation_regex) + 1
837 if (col > 0 && !s:IsInStringOrComment(lnum, col))
838 \ || s:IsInString(lnum, strlen(line))
839 let msl = lnum
840 else
841 break
842 endif
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000843 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200844
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000845 let lnum = s:PrevNonBlankNonString(lnum - 1)
846 endwhile
847 return msl
848endfunction
849
850" Check if line 'lnum' has more opening brackets than closing ones.
Bram Moolenaard09091d2019-01-17 16:07:22 +0100851function! s:ExtraBrackets(lnum) abort
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200852 let opening = {'parentheses': [], 'braces': [], 'brackets': []}
853 let closing = {'parentheses': [], 'braces': [], 'brackets': []}
854
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000855 let line = getline(a:lnum)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200856 let pos = match(line, '[][(){}]', 0)
857
858 " Save any encountered opening brackets, and remove them once a matching
859 " closing one has been found. If a closing bracket shows up that doesn't
860 " close anything, save it for later.
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000861 while pos != -1
862 if !s:IsInStringOrComment(a:lnum, pos + 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200863 if line[pos] == '('
864 call add(opening.parentheses, {'type': '(', 'pos': pos})
865 elseif line[pos] == ')'
866 if empty(opening.parentheses)
867 call add(closing.parentheses, {'type': ')', 'pos': pos})
868 else
869 let opening.parentheses = opening.parentheses[0:-2]
870 endif
871 elseif line[pos] == '{'
872 call add(opening.braces, {'type': '{', 'pos': pos})
873 elseif line[pos] == '}'
874 if empty(opening.braces)
875 call add(closing.braces, {'type': '}', 'pos': pos})
876 else
877 let opening.braces = opening.braces[0:-2]
878 endif
879 elseif line[pos] == '['
880 call add(opening.brackets, {'type': '[', 'pos': pos})
881 elseif line[pos] == ']'
882 if empty(opening.brackets)
883 call add(closing.brackets, {'type': ']', 'pos': pos})
884 else
885 let opening.brackets = opening.brackets[0:-2]
886 endif
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000887 endif
888 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200889
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000890 let pos = match(line, '[][(){}]', pos + 1)
891 endwhile
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200892
893 " Find the rightmost brackets, since they're the ones that are important in
894 " both opening and closing cases
895 let rightmost_opening = {'type': '(', 'pos': -1}
896 let rightmost_closing = {'type': ')', 'pos': -1}
897
898 for opening in opening.parentheses + opening.braces + opening.brackets
899 if opening.pos > rightmost_opening.pos
900 let rightmost_opening = opening
901 endif
902 endfor
903
904 for closing in closing.parentheses + closing.braces + closing.brackets
905 if closing.pos > rightmost_closing.pos
906 let rightmost_closing = closing
907 endif
908 endfor
909
910 return [rightmost_opening, rightmost_closing]
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000911endfunction
912
Bram Moolenaard09091d2019-01-17 16:07:22 +0100913function! s:Match(lnum, regex) abort
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200914 let line = getline(a:lnum)
915 let offset = match(line, '\C'.a:regex)
916 let col = offset + 1
917
918 while offset > -1 && s:IsInStringOrComment(a:lnum, col)
919 let offset = match(line, '\C'.a:regex, offset + 1)
920 let col = offset + 1
921 endwhile
922
923 if offset > -1
924 return col
925 else
926 return 0
927 endif
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000928endfunction
929
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200930" Locates the containing class/module's definition line, ignoring nested classes
931" along the way.
932"
Bram Moolenaard09091d2019-01-17 16:07:22 +0100933function! s:FindContainingClass() abort
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200934 let saved_position = getpos('.')
935
936 while searchpair(s:end_start_regex, s:end_middle_regex, s:end_end_regex, 'bW',
937 \ s:end_skip_expr) > 0
938 if expand('<cword>') =~# '\<class\|module\>'
939 let found_lnum = line('.')
940 call setpos('.', saved_position)
941 return found_lnum
942 endif
Bram Moolenaar45758762016-10-12 23:08:06 +0200943 endwhile
Bram Moolenaar89bcfda2016-08-30 23:26:57 +0200944
945 call setpos('.', saved_position)
946 return 0
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000947endfunction
948
Bram Moolenaar60a795a2005-09-16 21:55:43 +0000949" }}}1
950
951let &cpo = s:cpo_save
952unlet s:cpo_save
Bram Moolenaar9964e462007-05-05 17:54:07 +0000953
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200954" vim:set sw=2 sts=2 ts=8 et: